Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

elastic-apm-node

Package Overview
Dependencies
Maintainers
4
Versions
162
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

elastic-apm-node - npm Package Compare versions

Comparing version 3.26.0 to 3.27.0

15

lib/agent.js

@@ -129,2 +129,5 @@ 'use strict'

}
if (this._instrumentation._runCtxMgr && this._instrumentation._runCtxMgr._runContextFromAsyncId) {
stats.runContextFromAsyncIdSize = this._instrumentation._runCtxMgr._runContextFromAsyncId.size
}
if (this._transport && typeof this._transport._getStats === 'function') {

@@ -438,2 +441,8 @@ stats.apmclient = this._transport._getStats()

const traceContext = (span || trans || {})._context
const req = (opts.request instanceof IncomingMessage
? opts.request
: trans && trans.req)
const res = (opts.response instanceof ServerResponse
? opts.response
: trans && trans.res)

@@ -468,8 +477,2 @@ // As an added feature, for *some* cases, we capture a stacktrace at the point

// Gather `error.context.*`.
const req = (opts.request instanceof IncomingMessage
? opts.request
: trans && trans.req)
const res = (opts.response instanceof ServerResponse
? opts.response
: trans && trans.res)
const errorContext = {

@@ -476,0 +479,0 @@ user: Object.assign(

@@ -26,2 +26,3 @@ 'use strict'

this.ended = false
this._duration = null // Duration in milliseconds. Set on `.end()`.

@@ -87,3 +88,3 @@ this.outcome = constants.OUTCOME_UNKNOWN

// duration is returned as an int representing the time in milliseconds
// The duration of the span, in milliseconds.
GenericSpan.prototype.duration = function () {

@@ -95,3 +96,3 @@ if (!this.ended) {

return this._timer.duration
return this._duration
}

@@ -98,0 +99,0 @@

31

lib/instrumentation/modules/aws-sdk/dynamodb.js
'use strict'
const constants = require('../../../constants')
const TYPE = 'db'

@@ -66,8 +66,5 @@ const SUBTYPE = 'dynamodb'

const type = TYPE
const subtype = SUBTYPE
const action = ACTION
const ins = agent._instrumentation
const name = getSpanNameFromRequest(request)
const span = agent.startSpan(name, type, subtype, action)
const span = ins.createSpan(name, TYPE, SUBTYPE, ACTION)
if (!span) {

@@ -95,15 +92,19 @@ return orig.apply(request, origArguments)

request.on('complete', function (response) {
const onComplete = function (response) {
if (response && response.error) {
const errOpts = {
skipOutcome: true
}
agent.captureError(response.error, errOpts)
span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE)
agent.captureError(response.error)
}
span.end()
})
}
// Bind onComplete to the span's run context so that `captureError` picks
// up the correct currentSpan.
const parentRunContext = ins.currRunContext()
const spanRunContext = parentRunContext.enterSpan(span)
request.on('complete', ins.bindFunctionToRunContext(spanRunContext, onComplete))
return orig.apply(request, origArguments)
const cb = origArguments[origArguments.length - 1]
if (typeof cb === 'function') {
origArguments[origArguments.length - 1] = ins.bindFunctionToRunContext(parentRunContext, cb)
}
return ins.withRunContext(spanRunContext, orig, request, ...origArguments)
}

@@ -110,0 +111,0 @@

@@ -5,28 +5,51 @@ 'use strict'

module.exports = function (graphqlHTTP, agent, { version, enabled }) {
if (!enabled) return graphqlHTTP
const shimmer = require('../shimmer')
if (!semver.satisfies(version, '>=0.6.1 <0.10.0') || typeof graphqlHTTP !== 'function') {
agent.logger.debug('express-graphql version %s not supported - aborting...', version)
return graphqlHTTP
module.exports = function (expressGraphql, agent, { version, enabled }) {
if (!enabled) {
return expressGraphql
}
for (const key of Object.keys(graphqlHTTP)) {
wrappedGraphqlHTTP[key] = graphqlHTTP[key]
if (semver.satisfies(version, '>=0.10.0 <0.13.0')) {
// https://github.com/graphql/express-graphql/pull/626 changed `graphqlHTTP`
// to no longer be the top-level export:
// {
// graphqlHTTP: [Function: graphqlHTTP],
// getGraphQLParams: [AsyncFunction: getGraphQLParams]
// }
shimmer.wrap(expressGraphql, 'graphqlHTTP', wrapGraphqlHTTP)
return expressGraphql
} else if (semver.satisfies(version, '>=0.6.1 <0.10.0') && typeof expressGraphql === 'function') {
// Up to and including 0.9.x, `require('express-graphql')` is:
// [Function: graphqlHTTP] {
// getGraphQLParams: [AsyncFunction: getGraphQLParams]
// }
const wrappedGraphqlHTTP = wrapGraphqlHTTP(expressGraphql)
for (const key of Object.keys(expressGraphql)) {
wrappedGraphqlHTTP[key] = expressGraphql[key]
}
return wrappedGraphqlHTTP
} else {
agent.logger.debug('express-graphql@%s not supported: skipping instrumentation', version)
return expressGraphql
}
return wrappedGraphqlHTTP
function wrapGraphqlHTTP (origGraphqlHTTP) {
return function wrappedGraphqlHTTP () {
var orig = origGraphqlHTTP.apply(this, arguments)
function wrappedGraphqlHTTP () {
var orig = graphqlHTTP.apply(this, arguments)
if (typeof orig !== 'function') {
return orig
}
if (typeof orig !== 'function') return orig
// Express is very particular with the number of arguments!
return function (req, res) {
var trans = agent._instrumentation.currTransaction()
if (trans) trans._graphqlRoute = true
return orig.apply(this, arguments)
// Express is very particular with the number of arguments!
return function (req, res) {
var trans = agent._instrumentation.currTransaction()
if (trans) {
trans._graphqlRoute = true
}
return orig.apply(this, arguments)
}
}
}
}

@@ -20,2 +20,4 @@ 'use strict'

const ins = agent._instrumentation
return clone({}, graphql, {

@@ -44,12 +46,5 @@ graphql (descriptor) {

return function wrappedGraphql (schema, requestString, rootValue, contextValue, variableValues, operationName) {
var trans = agent._instrumentation.currTransaction()
var span = agent.startSpan('GraphQL: Unknown Query', 'db', 'graphql', 'execute')
var id = span && span.transaction.id
agent.logger.debug('intercepted call to graphql.graphql %o', { id: id })
// As of now, the only reason why there might be a transaction but no
// span is if the transaction have ended. But just to be sure this
// doesn't break in the future we add the extra `!span` guard as well
if (!trans || trans.ended || !span) {
agent.logger.debug('no active transaction found - skipping graphql tracing')
agent.logger.debug('intercepted call to graphql.graphql')
const span = ins.createSpan('GraphQL: Unknown Query', 'db', 'graphql', 'execute')
if (!span) {
return orig.apply(this, arguments)

@@ -79,3 +74,4 @@ }

var p = orig.apply(this, arguments)
const spanRunContext = ins.currRunContext().enterSpan(span)
const p = ins.withRunContext(spanRunContext, orig, this, ...arguments)
p.then(function () {

@@ -90,11 +86,5 @@ span.end()

function wrappedExecuteImpl (schema, document, rootValue, contextValue, variableValues, operationName) {
var trans = agent._instrumentation.currTransaction()
var span = agent.startSpan('GraphQL: Unknown Query', 'db', 'graphql', 'execute')
var id = span && span.transaction.id
agent.logger.debug('intercepted call to graphql.execute %o', { id: id })
// As of now, the only reason why there might be a transaction but no
// span is if the transaction have ended. But just to be sure this
// doesn't break in the future we add the extra `!span` guard as well
if (!trans || trans.ended || !span) {
agent.logger.debug('intercepted call to graphql.execute')
const span = ins.createSpan('GraphQL: Unknown Query', 'db', 'graphql', 'execute')
if (!span) {
agent.logger.debug('no active transaction found - skipping graphql tracing')

@@ -107,4 +97,10 @@ return orig.apply(this, arguments)

operationName = operationName || (details.operation && details.operation.name && details.operation.name.value)
if (queries.length > 0) span.name = 'GraphQL: ' + (operationName ? operationName + ' ' : '') + queries.join(', ')
if (queries.length > 0) {
span.name = 'GraphQL: ' + (operationName ? operationName + ' ' : '') + queries.join(', ')
}
// `_graphqlRoute` is a boolean set in instrumentations of other modules
// that specify 'graphql' in peerDependencies (e.g. 'express-graphql') to
// indicate that this transaction is for a GraphQL request.
const trans = span.transaction
if (trans._graphqlRoute) {

@@ -120,3 +116,4 @@ var name = queries.length > 0 ? queries.join(', ') : 'Unknown GraphQL query'

var p = orig.apply(this, arguments)
const spanRunContext = ins.currRunContext().enterSpan(span)
const p = ins.withRunContext(spanRunContext, orig, this, ...arguments)
if (typeof p.then === 'function') {

@@ -123,0 +120,0 @@ p.then(function () {

@@ -60,2 +60,6 @@ 'use strict'

trans.type = 'request'
// `trans.req` and `trans.res` are fake representations of Node.js's
// core `http.IncomingMessage` and `http.ServerResponse` objects,
// sufficient for `parsers.getContextFromRequest()` and
// `parsers.getContextFromResponse()`, respectively.
trans.req = {

@@ -168,3 +172,9 @@ headers,

var trans = agent._instrumentation.currTransaction()
if (trans) trans.res.finished = true
// `trans.res` might be removed, because before
// https://github.com/nodejs/node/pull/20084 (e.g. in node v10.0.0) the
// 'end' event could be called multiple times for the same Http2Stream,
// and the `trans.res` ref is removed when the Transaction is ended.
if (trans && trans.res) {
trans.res.finished = true
}
return original.apply(this, arguments)

@@ -171,0 +181,0 @@ }

'use strict'
const semver = require('semver')
const { getDBDestination } = require('../context')
const shimmer = require('../shimmer')

@@ -17,6 +19,7 @@ // Match expected `<hostname>:<port>`, e.g. "mongo:27017", "::1:27017",

const ins = agent._instrumentation
const activeSpans = new Map()
if (mongodb.instrument) {
const listener = mongodb.instrument()
listener.on('started', onStart)

@@ -27,12 +30,16 @@ listener.on('succeeded', onEnd)

// mongodb 4.0+ removed the instrument() method in favor of
// listeners on the instantiated client objects.
class MongoClient extends mongodb.MongoClient {
// listeners on the instantiated client objects. There are two mechanisms
// to get a client:
// 1. const client = new mongodb.MongoClient(...)
// 2. const client = await MongoClient.connect(...)
class MongoClientTraced extends mongodb.MongoClient {
constructor () {
// The `command*` events are only sent if `monitorCommands: true`.
if (!arguments[1]) {
arguments[1] = { monitorCommands: true }
} else if (arguments[1].monitorCommands !== true) {
arguments[1] = Object.assign({}, arguments[1], { monitorCommands: true })
// The `command*` events are only emitted if `options.monitorCommands: true`.
const args = Array.prototype.slice.call(arguments)
if (!args[1]) {
args[1] = { monitorCommands: true }
} else if (args[1].monitorCommands !== true) {
args[1] = Object.assign({}, args[1], { monitorCommands: true })
}
super(...arguments)
super(...args)
this.on('commandStarted', onStart)

@@ -49,9 +56,49 @@ this.on('commandSucceeded', onEnd)

get: function () {
return MongoClient
return MongoClientTraced
}
}
)
shimmer.wrap(mongodb.MongoClient, 'connect', wrapConnect)
} else {
agent.logger.warn('could not instrument mongodb@%s', version)
}
return mongodb
// Wrap the MongoClient.connect(url, options?, callback?) static method.
// It calls back with `function (err, client)` or returns a Promise that
// resolves to the client.
// https://github.com/mongodb/node-mongodb-native/blob/v4.2.1/src/mongo_client.ts#L503-L511
function wrapConnect (origConnect) {
return function wrappedConnect (url, options, callback) {
if (typeof options === 'function') {
callback = options
options = {}
}
options = options || {}
if (!options.monitorCommands) {
options.monitorCommands = true
}
if (typeof callback === 'function') {
return origConnect.call(this, url, options, function wrappedCallback (err, client) {
if (err) {
callback(err)
} else {
client.on('commandStarted', onStart)
client.on('commandSucceeded', onEnd)
client.on('commandFailed', onEnd)
callback(err, client)
}
})
} else {
const p = origConnect.call(this, url, options, callback)
p.then(client => {
client.on('commandStarted', onStart)
client.on('commandSucceeded', onEnd)
client.on('commandFailed', onEnd)
})
return p
}
}
}
function onStart (event) {

@@ -76,3 +123,3 @@ // `event` is a `CommandStartedEvent`

const span = agent.startSpan(name, 'db', 'mongodb', 'query')
const span = ins.createSpan(name, 'db', 'mongodb', 'query')
if (span) {

@@ -79,0 +126,0 @@ activeSpans.set(event.requestId, span)

@@ -13,2 +13,5 @@ 'use strict'

module.exports = function (pg, agent, { version, enabled }) {
if (!enabled) {
return pg
}
if (!semver.satisfies(version, '>=4.0.0 <9.0.0')) {

@@ -19,3 +22,3 @@ agent.logger.debug('pg version %s not supported - aborting...', version)

patchClient(pg.Client, 'pg.Client', agent, enabled)
patchClient(pg.Client, 'pg.Client', agent)

@@ -34,3 +37,3 @@ // Trying to access the pg.native getter will trigger and log the warning

if (native && native.Client) {
patchClient(native.Client, 'pg.native.Client', agent, enabled)
patchClient(native.Client, 'pg.native.Client', agent)
}

@@ -44,5 +47,3 @@ return native

function patchClient (Client, klass, agent, enabled) {
if (!enabled) return
function patchClient (Client, klass, agent) {
agent.logger.debug('shimming %s.prototype.query', klass)

@@ -53,74 +54,73 @@ shimmer.wrap(Client.prototype, 'query', wrapQuery)

return function wrappedFunction (sql) {
var span = agent.startSpan('SQL', 'db', 'postgresql', 'query')
var id = span && span.transaction.id
agent.logger.debug('intercepted call to %s.prototype.%s', klass, name)
const ins = agent._instrumentation
const span = ins.createSpan('SQL', 'db', 'postgresql', 'query')
if (!span) {
return orig.apply(this, arguments)
}
if (sql && typeof sql.text === 'string') sql = sql.text
let sqlText = sql
if (sql && typeof sql.text === 'string') {
sqlText = sql.text
}
if (typeof sqlText === 'string') {
span.setDbContext({ statement: sqlText, type: 'sql' })
span.name = sqlSummary(sqlText)
} else {
agent.logger.debug('unable to parse sql form pg module (type: %s)', typeof sqlText)
}
agent.logger.debug('intercepted call to %s.prototype.%s %o', klass, name, { id: id, sql: sql })
// Get connection parameters from Client.
let host, port
if (typeof this.connectionParameters === 'object') {
({ host, port } = this.connectionParameters)
}
span.setDestinationContext(getDBDestination(span, host, port))
if (span) {
// get connection parameters from Client
let host, port
if (typeof this.connectionParameters === 'object') {
({ host, port } = this.connectionParameters)
}
span.setDestinationContext(getDBDestination(span, host, port))
if (this[symbols.knexStackObj]) {
span.customStackTrace(this[symbols.knexStackObj])
this[symbols.knexStackObj] = null
}
var args = arguments
var index = args.length - 1
var cb = args[index]
let index = arguments.length - 1
let cb = arguments[index]
if (Array.isArray(cb)) {
index = cb.length - 1
cb = cb[index]
}
if (this[symbols.knexStackObj]) {
span.customStackTrace(this[symbols.knexStackObj])
this[symbols.knexStackObj] = null
}
const spanRunContext = ins.currRunContext().enterSpan(span)
const onQueryEnd = ins.bindFunctionToRunContext(spanRunContext, (_err) => {
agent.logger.debug('intercepted end of %s.prototype.%s', klass, name)
span.end()
})
if (Array.isArray(cb)) {
index = cb.length - 1
cb = cb[index]
}
if (typeof cb === 'function') {
arguments[index] = ins.bindFunction((err, res) => {
onQueryEnd(err)
return cb(err, res)
})
return orig.apply(this, arguments)
} else {
var queryOrPromise = orig.apply(this, arguments)
if (typeof sql === 'string') {
span.setDbContext({ statement: sql, type: 'sql' })
span.name = sqlSummary(sql)
// It is important to prefer `.on` to `.then` for pg <7 >=6.3.0, because
// `query.then` is broken in those versions. See
// https://github.com/brianc/node-postgres/commit/b5b49eb895727e01290e90d08292c0d61ab86322#r23267714
if (typeof queryOrPromise.on === 'function') {
queryOrPromise.on('end', onQueryEnd)
queryOrPromise.on('error', onQueryEnd)
if (queryOrPromise instanceof EventEmitter) {
ins.bindEmitter(queryOrPromise)
}
} else if (typeof queryOrPromise.then === 'function') {
queryOrPromise.then(
() => { onQueryEnd() },
onQueryEnd
)
} else {
agent.logger.debug('unable to parse sql form pg module (type: %s)', typeof sql)
agent.logger.debug('ERROR: unknown pg query type: %s', typeof queryOrPromise)
}
const onQueryEnd = (_err) => {
agent.logger.debug('intercepted end of %s.prototype.%s %o', klass, name, { id: id })
span.end()
}
if (typeof cb === 'function') {
args[index] = agent._instrumentation.bindFunction((err, res) => {
onQueryEnd(err)
return cb(err, res)
})
return orig.apply(this, arguments)
} else {
var queryOrPromise = orig.apply(this, arguments)
// It is import to prefer `.on` to `.then` for pg <7 >=6.3.0, because
// `query.then` is broken in those versions. See
// https://github.com/brianc/node-postgres/commit/b5b49eb895727e01290e90d08292c0d61ab86322#r23267714
if (typeof queryOrPromise.on === 'function') {
queryOrPromise.on('end', onQueryEnd)
queryOrPromise.on('error', onQueryEnd)
if (queryOrPromise instanceof EventEmitter) {
agent._instrumentation.bindEmitter(queryOrPromise)
}
} else if (typeof queryOrPromise.then === 'function') {
queryOrPromise.then(
() => { onQueryEnd() },
onQueryEnd
)
} else {
agent.logger.debug('ERROR: unknown pg query type: %s %o', typeof queryOrPromise, { id: id })
}
return queryOrPromise
}
} else {
return orig.apply(this, arguments)
return queryOrPromise
}

@@ -127,0 +127,0 @@ }

@@ -77,2 +77,3 @@ 'use strict'

this._timer.end(endTime)
this._duration = this._timer.duration
if (executionAsyncId() !== this._startXid) {

@@ -88,3 +89,3 @@ this.sync = false

// TODO: This is expensive! Consider if there's a way to cache some of this
if (this._capturedStackTrace !== null && this._agent._conf.spanFramesMinDuration !== 0 && this.duration() / 1000 > this._agent._conf.spanFramesMinDuration) {
if (this._capturedStackTrace !== null && this._agent._conf.spanFramesMinDuration !== 0 && this._duration / 1000 > this._agent._conf.spanFramesMinDuration) {
// NOTE: This uses a promise-like thing and not a *real* promise

@@ -110,2 +111,15 @@ // because passing error stacks into a promise context makes it

this.transaction._captureBreakdown(this)
// Reduce this span's memory usage by dropping references, except to fields
// required to support `interface Span`.
// Span fields:
this._db = null
this._http = null
this._destination = null
this._message = null
this._capturedStackTrace = null
// GenericSpan fields:
// - Cannot drop `this._context` because it is used for traceparent and ids.
this._timer = null
this._labels = null
}

@@ -197,2 +211,35 @@

const payload = {
id: self.id,
transaction_id: self.transaction.id,
parent_id: self.parentId,
trace_id: self.traceId,
name: self.name,
type: self.type || 'custom',
subtype: self.subtype,
action: self.action,
timestamp: self.timestamp,
duration: self._duration,
context: undefined,
stacktrace: undefined,
sync: self.sync,
outcome: self.outcome
}
// if a valid sample rate is set (truthy or zero), set the property
const sampleRate = self.sampleRate
if (sampleRate !== null) {
payload.sample_rate = sampleRate
}
if (self._db || self._http || self._labels || self._destination || self._message) {
payload.context = {
db: self._db || undefined,
http: self._http || undefined,
tags: self._labels || undefined,
destination: self._destination || undefined,
message: self._message || undefined
}
}
if (this._agent._conf.captureSpanStackTraces && this._stackObj) {

@@ -210,37 +257,5 @@ this._stackObj.then(

self._agent.logger.debug('could not capture stack trace for span %o', { span: self.id, parent: self.parentId, trace: self.traceId, name: self.name, type: self.type, subtype: self.subtype, action: self.action, err: err.message })
} else if (frames) {
payload.stacktrace = frames
}
var payload = {
id: self.id,
transaction_id: self.transaction.id,
parent_id: self.parentId,
trace_id: self.traceId,
name: self.name,
type: self.type || 'custom',
subtype: self.subtype,
action: self.action,
timestamp: self.timestamp,
duration: self.duration(),
context: undefined,
stacktrace: frames,
sync: self.sync,
outcome: self.outcome
}
// if a valid sample rate is set (truthy or zero), set the property
const sampleRate = self.sampleRate
if (sampleRate !== null) {
payload.sample_rate = sampleRate
}
if (self._db || self._http || self._labels || self._destination || self._message) {
payload.context = {
db: self._db || undefined,
http: self._http || undefined,
tags: self._labels || undefined,
destination: self._destination || undefined,
message: self._message || undefined
}
}
cb(null, payload)

@@ -247,0 +262,0 @@ }

@@ -32,3 +32,2 @@ 'use strict'

this._droppedSpans = 0
this._abortTime = 0
this._breakdownTimings = new ObjectIdentityMap()

@@ -38,2 +37,3 @@ this._faas = undefined

this._message = undefined
this._cloud = undefined
this.outcome = constants.OUTCOME_UNKNOWN

@@ -151,2 +151,4 @@ }

// Note that this only returns a complete result when called *during* the call
// to `transaction.end()`.
Transaction.prototype.toJSON = function () {

@@ -159,3 +161,3 @@ var payload = {

type: this.type || 'custom',
duration: this.duration(),
duration: this._duration,
timestamp: this.timestamp,

@@ -216,2 +218,4 @@ result: String(this.result),

// Note that this only returns a complete result when called *during* the call
// to `transaction.end()`.
Transaction.prototype._encode = function () {

@@ -270,2 +274,3 @@ if (!this.ended) {

this._timer.end(endTime)
this._duration = this._timer.duration
this._captureBreakdown(this)

@@ -276,2 +281,22 @@ this.ended = true

this._agent.logger.debug('ended transaction %o', { trans: this.id, parent: this.parentId, trace: this.traceId, type: this.type, result: this.result, name: this.name })
// Reduce this transaction's memory usage by dropping references except to
// fields required to support `interface Transaction`.
// Transaction fields:
this._customName = this.name // Short-circuit the `name` getter.
this._defaultName = ''
this.req = null
this.res = null
this._user = null
this._custom = null
this._breakdownTimings = null
this._faas = undefined
this._service = undefined
this._message = undefined
this._cloud = undefined
// GenericSpan fields:
// - Cannot drop `this._context` because it is used for `traceparent`, `ids`,
// and `.sampled` (when capturing breakdown metrics for child spans).
this._timer = null
this._labels = null
}

@@ -278,0 +303,0 @@

{
"name": "elastic-apm-node",
"version": "3.26.0",
"version": "3.27.0",
"description": "The official Elastic APM agent for Node.js",

@@ -143,3 +143,3 @@ "main": "index.js",

"express": "^4.17.1",
"express-graphql": "^0.9.0",
"express-graphql": "^0.12.0",
"express-queue": "^0.0.12",

@@ -152,3 +152,3 @@ "fastify": "^2.12.0",

"got": "^9.6.0",
"graphql": "^15.3.0",
"graphql": "^15.8.0",
"handlebars": "^4.7.3",

@@ -168,3 +168,3 @@ "hapi": "^18.1.0",

"module-details-from-path": "^1.0.3",
"mongodb": "^4.1.0",
"mongodb": "^4.2.1",
"mongodb-core": "^3.2.7",

@@ -171,0 +171,0 @@ "mysql": "^2.18.1",

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc