{"version":3,"file":"redis-dc-subscriber.js","sources":["../../../src/redis/redis-dc-subscriber.ts"],"sourcesContent":["import type { Span } from '@sentry/core';\nimport {\n debug,\n SEMANTIC_ATTRIBUTE_SENTRY_OP,\n SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,\n SPAN_STATUS_ERROR,\n startSpanManual,\n} from '@sentry/core';\nimport { DEBUG_BUILD } from '../debug-build';\n\n// Channel names published by node-redis >= 5.12.0 and ioredis >= 5.11.0.\n// Hardcoded so the subscriber does not have to import either library — the\n// channels just have to be subscribed to before the user's redis code\n// publishes.\nexport const REDIS_DC_CHANNEL_COMMAND = 'node-redis:command';\nexport const REDIS_DC_CHANNEL_BATCH = 'node-redis:batch';\nexport const REDIS_DC_CHANNEL_CONNECT = 'node-redis:connect';\nexport const IOREDIS_DC_CHANNEL_COMMAND = 'ioredis:command';\nexport const IOREDIS_DC_CHANNEL_CONNECT = 'ioredis:connect';\n\nconst ORIGIN = 'auto.db.redis.diagnostic_channel';\n\n// Inlined semconv attribute keys — these are plain strings, no need to depend\n// on @opentelemetry/semantic-conventions for them.\nconst ATTR_DB_STATEMENT = 'db.statement';\nconst ATTR_DB_SYSTEM = 'db.system';\nconst ATTR_NET_PEER_NAME = 'net.peer.name';\nconst ATTR_NET_PEER_PORT = 'net.peer.port';\nconst DB_SYSTEM_VALUE_REDIS = 'redis';\n\nconst NOOP = (): void => {};\n\n/**\n * Shape of the `node-redis:command` channel payload published by node-redis.\n *\n * Both `command` and `args` are already redacted by node-redis itself (see\n * `sanitizeArgs` in @redis/client) using the OTel `redis-common` rules. The\n * arg array is `[, , ..., '?', ...]` — `?` replaces any\n * value the library considers sensitive. Subscribers can emit `args` directly\n * as `db.statement` without further serialization.\n */\nexport interface RedisCommandData {\n command: string;\n /** First arg is the command name itself; consumers should slice it off. */\n args: string[];\n database?: number;\n serverAddress?: string;\n serverPort?: number;\n result?: unknown;\n error?: Error;\n}\n\n/**\n * Shape of the `ioredis:command` channel payload published by ioredis >= 5.11.0.\n *\n * As with node-redis, args are already sanitized by ioredis (`sanitizeArgs` in\n * `lib/tracing.ts`) before publishing. Unlike node-redis, the command name is\n * NOT prepended to `args`.\n */\nexport interface IORedisCommandData {\n command: string;\n args: string[];\n batchMode?: 'MULTI';\n batchSize?: number;\n database?: number;\n serverAddress?: string;\n serverPort?: number;\n result?: unknown;\n error?: Error;\n}\n\n/** Shape of the `node-redis:batch` channel payload published by node-redis. */\nexport interface RedisBatchData {\n batchMode?: 'MULTI' | 'PIPELINE';\n batchSize?: number;\n database?: number;\n clientId?: string | number;\n serverAddress?: string;\n serverPort?: number;\n result?: unknown[];\n error?: Error;\n}\n\n/** Shape of the `*:connect` channel payload published by node-redis and ioredis. */\nexport interface RedisConnectData {\n serverAddress?: string;\n serverPort?: number;\n url?: string;\n error?: Error;\n}\n\n/**\n * Optional callback invoked once the redis command response arrives. Useful\n * for attaching response-derived attributes (e.g. cache hit/miss, payload size).\n *\n * Mirrors `@opentelemetry/instrumentation-ioredis`' response hook so existing\n * Sentry node code (`cacheResponseHook`) can be reused unchanged.\n */\nexport type RedisDiagnosticChannelResponseHook = (\n span: Span,\n cmdName: string,\n cmdArgs: string[],\n result: unknown,\n) => void;\n\n/**\n * Payload type observed by tracing-channel subscribers — the channel payload\n * with `_sentrySpan` stamped on it by the start handler so async/error\n * handlers downstream can read it back.\n */\nexport type RedisTracingChannelContextWithSpan = T & { _sentrySpan?: Span };\n\n/** Subscriber object accepted by {@link RedisTracingChannel.subscribe}. */\nexport interface RedisTracingChannelSubscribers {\n start: (data: RedisTracingChannelContextWithSpan) => void;\n asyncStart: (data: RedisTracingChannelContextWithSpan) => void;\n asyncEnd: (data: RedisTracingChannelContextWithSpan) => void;\n end: (data: RedisTracingChannelContextWithSpan) => void;\n error: (data: RedisTracingChannelContextWithSpan) => void;\n}\n\n/** Minimal tracing-channel surface the subscriber depends on. */\nexport interface RedisTracingChannel {\n subscribe(subs: Partial>): void;\n}\n\n/**\n * Platform-provided factory that returns a tracing channel for the given\n * channel name. Implementations are responsible for ensuring that, when the\n * channel's `start` event fires, the span returned by `transformStart(data)`\n * ends up stored on `data._sentrySpan` so the subscriber's `asyncEnd`/`error`\n * handlers can read it.\n *\n * - Node passes `@sentry/opentelemetry/tracing-channel` which uses\n * `bindStore` to also propagate the span as the active OTel context.\n * - Deno (and other non-OTel runtimes) pass a portable wrapper around\n * `node:diagnostics_channel.tracingChannel` that just stamps\n * `data._sentrySpan` in `start` without `bindStore`.\n */\nexport type RedisTracingChannelFactory = (\n name: string,\n transformStart: (data: T) => Span,\n) => RedisTracingChannel;\n\nlet subscribed = false;\nlet currentResponseHook: RedisDiagnosticChannelResponseHook | undefined;\n\n/**\n * Subscribe Sentry span handlers to node-redis and ioredis diagnostics-channel\n * events: `node-redis:command`/`:batch`/`:connect` (published by node-redis\n * >= 5.12.0) and `ioredis:command`/`:connect` (published by ioredis >= 5.11.0).\n *\n * On older client versions the channels are never published to, so subscribers\n * are inert — there is no double-instrumentation against any IITM-based\n * patcher gated to those older versions.\n *\n * Idempotent: subsequent calls update the response hook but do not\n * re-subscribe.\n */\nexport function subscribeRedisDiagnosticChannels(\n tracingChannel: RedisTracingChannelFactory,\n responseHook?: RedisDiagnosticChannelResponseHook,\n): void {\n currentResponseHook = responseHook;\n if (subscribed) return;\n subscribed = true;\n\n try {\n // node-redis: command name appears as args[0] in the channel payload, so\n // strip it before the statement and response hook see it.\n setupCommandChannel(tracingChannel, REDIS_DC_CHANNEL_COMMAND, data => data.args.slice(1));\n setupBatchChannel(tracingChannel, REDIS_DC_CHANNEL_BATCH, data =>\n data.batchMode === 'PIPELINE' ? 'PIPELINE' : 'MULTI',\n );\n setupConnectChannel(tracingChannel, REDIS_DC_CHANNEL_CONNECT);\n\n // ioredis: args already exclude the command name; no slicing needed. And\n // ioredis has no separate batch channel — pipeline/MULTI metadata rides\n // on the per-command payload via `batchMode`/`batchSize`.\n setupCommandChannel(tracingChannel, IOREDIS_DC_CHANNEL_COMMAND, data => data.args);\n setupConnectChannel(tracingChannel, IOREDIS_DC_CHANNEL_CONNECT);\n } catch {\n // The factory may rely on `node:diagnostics_channel`, which isn't always\n // available. Fail closed; the SDK simply won't emit redis spans here.\n DEBUG_BUILD && debug.log('Redis node:diagnostics_channel subscription failed.');\n }\n}\n\nfunction setupCommandChannel(\n tracingChannel: RedisTracingChannelFactory,\n channelName: string,\n getCommandArgs: (data: T) => string[],\n): void {\n const channel = tracingChannel(channelName, data => {\n // `args` is already sanitized by the publishing library (node-redis /\n // ioredis call their own `sanitizeArgs` before publishing). Join with\n // spaces to mirror the format the libraries themselves intend.\n const args = getCommandArgs(data);\n const statement = args.length ? `${data.command} ${args.join(' ')}` : data.command;\n return startSpanManual(\n {\n name: `redis-${data.command}`,\n attributes: {\n [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,\n [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db.redis',\n [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_REDIS,\n [ATTR_DB_STATEMENT]: statement,\n ...(data.serverAddress != null ? { [ATTR_NET_PEER_NAME]: data.serverAddress } : {}),\n ...(data.serverPort != null ? { [ATTR_NET_PEER_PORT]: data.serverPort } : {}),\n },\n },\n span => span,\n );\n });\n\n channel.subscribe({\n start: NOOP,\n asyncStart: NOOP,\n end: NOOP,\n asyncEnd: data => {\n const span = data._sentrySpan;\n // Only end here if the error handler isn't going to.\n if (!span || data.error) return;\n runResponseHook(span, data.command, getCommandArgs(data), data.result);\n span.end();\n },\n error: data => {\n const span = data._sentrySpan;\n if (!span) return;\n if (data.error) {\n span.setStatus({ code: SPAN_STATUS_ERROR, message: data.error.message });\n }\n span.end();\n },\n });\n}\n\nfunction setupBatchChannel(\n tracingChannel: RedisTracingChannelFactory,\n channelName: string,\n getOperationName: (data: RedisBatchData) => string,\n): void {\n const channel = tracingChannel(channelName, data => {\n return startSpanManual(\n {\n name: getOperationName(data),\n attributes: {\n [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,\n [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db.redis',\n [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_REDIS,\n ...(data.batchSize != null ? { 'db.redis.batch_size': data.batchSize } : {}),\n ...(data.serverAddress != null ? { [ATTR_NET_PEER_NAME]: data.serverAddress } : {}),\n ...(data.serverPort != null ? { [ATTR_NET_PEER_PORT]: data.serverPort } : {}),\n },\n },\n span => span,\n );\n });\n\n channel.subscribe({\n start: NOOP,\n asyncStart: NOOP,\n end: NOOP,\n asyncEnd: data => {\n if (!data.error) data._sentrySpan?.end();\n },\n error: data => {\n const span = data._sentrySpan;\n if (!span) return;\n if (data.error) {\n span.setStatus({ code: SPAN_STATUS_ERROR, message: data.error.message });\n }\n span.end();\n },\n });\n}\n\nfunction setupConnectChannel(tracingChannel: RedisTracingChannelFactory, channelName: string): void {\n const channel = tracingChannel(channelName, data => {\n return startSpanManual(\n {\n name: 'redis-connect',\n attributes: {\n [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,\n [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db.redis.connect',\n [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_REDIS,\n ...(data.serverAddress != null ? { [ATTR_NET_PEER_NAME]: data.serverAddress } : {}),\n ...(data.serverPort != null ? { [ATTR_NET_PEER_PORT]: data.serverPort } : {}),\n },\n },\n span => span,\n );\n });\n\n channel.subscribe({\n start: NOOP,\n asyncStart: NOOP,\n end: NOOP,\n asyncEnd: data => {\n if (!data.error) data._sentrySpan?.end();\n },\n error: data => {\n const span = data._sentrySpan;\n if (!span) return;\n if (data.error) {\n span.setStatus({ code: SPAN_STATUS_ERROR, message: data.error.message });\n }\n span.end();\n },\n });\n}\n\nfunction runResponseHook(span: Span, command: string, args: string[], result: unknown): void {\n const hook = currentResponseHook;\n if (!hook) return;\n try {\n hook(span, command, args, result);\n } catch {\n // never let user hooks break instrumentation\n }\n}\n\n/** Test-only: reset module-local subscribe state. */\nexport function _resetRedisDiagnosticChannelsForTesting(): void {\n subscribed = false;\n currentResponseHook = undefined;\n}\n"],"names":["DEBUG_BUILD","debug","startSpanManual","SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN","SEMANTIC_ATTRIBUTE_SENTRY_OP","SPAN_STATUS_ERROR"],"mappings":";;;;;AAcO,MAAM,wBAAA,GAA2B;AACjC,MAAM,sBAAA,GAAyB;AAC/B,MAAM,wBAAA,GAA2B;AACjC,MAAM,0BAAA,GAA6B;AACnC,MAAM,0BAAA,GAA6B;AAE1C,MAAM,MAAA,GAAS,kCAAA;AAIf,MAAM,iBAAA,GAAoB,cAAA;AAC1B,MAAM,cAAA,GAAiB,WAAA;AACvB,MAAM,kBAAA,GAAqB,eAAA;AAC3B,MAAM,kBAAA,GAAqB,eAAA;AAC3B,MAAM,qBAAA,GAAwB,OAAA;AAE9B,MAAM,OAAO,MAAY;AAAC,CAAA;AAkH1B,IAAI,UAAA,GAAa,KAAA;AACjB,IAAI,mBAAA;AAcG,SAAS,gCAAA,CACd,gBACA,YAAA,EACM;AACN,EAAA,mBAAA,GAAsB,YAAA;AACtB,EAAA,IAAI,UAAA,EAAY;AAChB,EAAA,UAAA,GAAa,IAAA;AAEb,EAAA,IAAI;AAGF,IAAA,mBAAA,CAAsC,gBAAgB,wBAAA,EAA0B,CAAA,IAAA,KAAQ,KAAK,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC,CAAA;AAC1G,IAAA,iBAAA;AAAA,MAAkB,cAAA;AAAA,MAAgB,sBAAA;AAAA,MAAwB,CAAA,IAAA,KACxD,IAAA,CAAK,SAAA,KAAc,UAAA,GAAa,UAAA,GAAa;AAAA,KAC/C;AACA,IAAA,mBAAA,CAAoB,gBAAgB,wBAAwB,CAAA;AAK5D,IAAA,mBAAA,CAAwC,cAAA,EAAgB,0BAAA,EAA4B,CAAA,IAAA,KAAQ,IAAA,CAAK,IAAI,CAAA;AACrG,IAAA,mBAAA,CAAoB,gBAAgB,0BAA0B,CAAA;AAAA,EAChE,CAAA,CAAA,MAAQ;AAGN,IAAAA,sBAAA,IAAeC,UAAA,CAAM,IAAI,qDAAqD,CAAA;AAAA,EAChF;AACF;AAEA,SAAS,mBAAA,CACP,cAAA,EACA,WAAA,EACA,cAAA,EACM;AACN,EAAA,MAAM,OAAA,GAAU,cAAA,CAAkB,WAAA,EAAa,CAAA,IAAA,KAAQ;AAIrD,IAAA,MAAM,IAAA,GAAO,eAAe,IAAI,CAAA;AAChC,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,MAAA,GAAS,CAAA,EAAG,IAAA,CAAK,OAAO,CAAA,CAAA,EAAI,IAAA,CAAK,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA,GAAK,IAAA,CAAK,OAAA;AAC3E,IAAA,OAAOC,oBAAA;AAAA,MACL;AAAA,QACE,IAAA,EAAM,CAAA,MAAA,EAAS,IAAA,CAAK,OAAO,CAAA,CAAA;AAAA,QAC3B,UAAA,EAAY;AAAA,UACV,CAACC,qCAAgC,GAAG,MAAA;AAAA,UACpC,CAACC,iCAA4B,GAAG,UAAA;AAAA,UAChC,CAAC,cAAc,GAAG,qBAAA;AAAA,UAClB,CAAC,iBAAiB,GAAG,SAAA;AAAA,UACrB,GAAI,IAAA,CAAK,aAAA,IAAiB,IAAA,GAAO,EAAE,CAAC,kBAAkB,GAAG,IAAA,CAAK,aAAA,EAAc,GAAI,EAAC;AAAA,UACjF,GAAI,IAAA,CAAK,UAAA,IAAc,IAAA,GAAO,EAAE,CAAC,kBAAkB,GAAG,IAAA,CAAK,UAAA,EAAW,GAAI;AAAC;AAC7E,OACF;AAAA,MACA,CAAA,IAAA,KAAQ;AAAA,KACV;AAAA,EACF,CAAC,CAAA;AAED,EAAA,OAAA,CAAQ,SAAA,CAAU;AAAA,IAChB,KAAA,EAAO,IAAA;AAAA,IACP,UAAA,EAAY,IAAA;AAAA,IACZ,GAAA,EAAK,IAAA;AAAA,IACL,UAAU,CAAA,IAAA,KAAQ;AAChB,MAAA,MAAM,OAAO,IAAA,CAAK,WAAA;AAElB,MAAA,IAAI,CAAC,IAAA,IAAQ,IAAA,CAAK,KAAA,EAAO;AACzB,MAAA,eAAA,CAAgB,MAAM,IAAA,CAAK,OAAA,EAAS,eAAe,IAAI,CAAA,EAAG,KAAK,MAAM,CAAA;AACrE,MAAA,IAAA,CAAK,GAAA,EAAI;AAAA,IACX,CAAA;AAAA,IACA,OAAO,CAAA,IAAA,KAAQ;AACb,MAAA,MAAM,OAAO,IAAA,CAAK,WAAA;AAClB,MAAA,IAAI,CAAC,IAAA,EAAM;AACX,MAAA,IAAI,KAAK,KAAA,EAAO;AACd,QAAA,IAAA,CAAK,SAAA,CAAU,EAAE,IAAA,EAAMC,sBAAA,EAAmB,SAAS,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA;AAAA,MACzE;AACA,MAAA,IAAA,CAAK,GAAA,EAAI;AAAA,IACX;AAAA,GACD,CAAA;AACH;AAEA,SAAS,iBAAA,CACP,cAAA,EACA,WAAA,EACA,gBAAA,EACM;AACN,EAAA,MAAM,OAAA,GAAU,cAAA,CAA+B,WAAA,EAAa,CAAA,IAAA,KAAQ;AAClE,IAAA,OAAOH,oBAAA;AAAA,MACL;AAAA,QACE,IAAA,EAAM,iBAAiB,IAAI,CAAA;AAAA,QAC3B,UAAA,EAAY;AAAA,UACV,CAACC,qCAAgC,GAAG,MAAA;AAAA,UACpC,CAACC,iCAA4B,GAAG,UAAA;AAAA,UAChC,CAAC,cAAc,GAAG,qBAAA;AAAA,UAClB,GAAI,KAAK,SAAA,IAAa,IAAA,GAAO,EAAE,qBAAA,EAAuB,IAAA,CAAK,SAAA,EAAU,GAAI,EAAC;AAAA,UAC1E,GAAI,IAAA,CAAK,aAAA,IAAiB,IAAA,GAAO,EAAE,CAAC,kBAAkB,GAAG,IAAA,CAAK,aAAA,EAAc,GAAI,EAAC;AAAA,UACjF,GAAI,IAAA,CAAK,UAAA,IAAc,IAAA,GAAO,EAAE,CAAC,kBAAkB,GAAG,IAAA,CAAK,UAAA,EAAW,GAAI;AAAC;AAC7E,OACF;AAAA,MACA,CAAA,IAAA,KAAQ;AAAA,KACV;AAAA,EACF,CAAC,CAAA;AAED,EAAA,OAAA,CAAQ,SAAA,CAAU;AAAA,IAChB,KAAA,EAAO,IAAA;AAAA,IACP,UAAA,EAAY,IAAA;AAAA,IACZ,GAAA,EAAK,IAAA;AAAA,IACL,UAAU,CAAA,IAAA,KAAQ;AAChB,MAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,IAAA,CAAK,aAAa,GAAA,EAAI;AAAA,IACzC,CAAA;AAAA,IACA,OAAO,CAAA,IAAA,KAAQ;AACb,MAAA,MAAM,OAAO,IAAA,CAAK,WAAA;AAClB,MAAA,IAAI,CAAC,IAAA,EAAM;AACX,MAAA,IAAI,KAAK,KAAA,EAAO;AACd,QAAA,IAAA,CAAK,SAAA,CAAU,EAAE,IAAA,EAAMC,sBAAA,EAAmB,SAAS,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA;AAAA,MACzE;AACA,MAAA,IAAA,CAAK,GAAA,EAAI;AAAA,IACX;AAAA,GACD,CAAA;AACH;AAEA,SAAS,mBAAA,CAAoB,gBAA4C,WAAA,EAA2B;AAClG,EAAA,MAAM,OAAA,GAAU,cAAA,CAAiC,WAAA,EAAa,CAAA,IAAA,KAAQ;AACpE,IAAA,OAAOH,oBAAA;AAAA,MACL;AAAA,QACE,IAAA,EAAM,eAAA;AAAA,QACN,UAAA,EAAY;AAAA,UACV,CAACC,qCAAgC,GAAG,MAAA;AAAA,UACpC,CAACC,iCAA4B,GAAG,kBAAA;AAAA,UAChC,CAAC,cAAc,GAAG,qBAAA;AAAA,UAClB,GAAI,IAAA,CAAK,aAAA,IAAiB,IAAA,GAAO,EAAE,CAAC,kBAAkB,GAAG,IAAA,CAAK,aAAA,EAAc,GAAI,EAAC;AAAA,UACjF,GAAI,IAAA,CAAK,UAAA,IAAc,IAAA,GAAO,EAAE,CAAC,kBAAkB,GAAG,IAAA,CAAK,UAAA,EAAW,GAAI;AAAC;AAC7E,OACF;AAAA,MACA,CAAA,IAAA,KAAQ;AAAA,KACV;AAAA,EACF,CAAC,CAAA;AAED,EAAA,OAAA,CAAQ,SAAA,CAAU;AAAA,IAChB,KAAA,EAAO,IAAA;AAAA,IACP,UAAA,EAAY,IAAA;AAAA,IACZ,GAAA,EAAK,IAAA;AAAA,IACL,UAAU,CAAA,IAAA,KAAQ;AAChB,MAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,IAAA,CAAK,aAAa,GAAA,EAAI;AAAA,IACzC,CAAA;AAAA,IACA,OAAO,CAAA,IAAA,KAAQ;AACb,MAAA,MAAM,OAAO,IAAA,CAAK,WAAA;AAClB,MAAA,IAAI,CAAC,IAAA,EAAM;AACX,MAAA,IAAI,KAAK,KAAA,EAAO;AACd,QAAA,IAAA,CAAK,SAAA,CAAU,EAAE,IAAA,EAAMC,sBAAA,EAAmB,SAAS,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA;AAAA,MACzE;AACA,MAAA,IAAA,CAAK,GAAA,EAAI;AAAA,IACX;AAAA,GACD,CAAA;AACH;AAEA,SAAS,eAAA,CAAgB,IAAA,EAAY,OAAA,EAAiB,IAAA,EAAgB,MAAA,EAAuB;AAC3F,EAAA,MAAM,IAAA,GAAO,mBAAA;AACb,EAAA,IAAI,CAAC,IAAA,EAAM;AACX,EAAA,IAAI;AACF,IAAA,IAAA,CAAK,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,MAAM,CAAA;AAAA,EAClC,CAAA,CAAA,MAAQ;AAAA,EAER;AACF;;;;;;;;;"}