From b74a5c48ee256a52cad7dd7962cb612769f40900 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 15 Jun 2026 14:51:54 -0400 Subject: [PATCH] ref(node): Streamline ioredis instrumentation Streamlines the vendored `ioredis` instrumentation to use Sentry's span APIs instead of the OpenTelemetry tracing API, and removes the code paths that are dead in Sentry's context. Mirrors the approach in #21481 (mongoose) and #21509 (mysql2). ### Notes #### Blockers for `startSpan*` ioredis ends its span manually since completion fires via the patched `cmd.resolve`/`cmd.reject` (and the `connect` promise) after the sync wrapper returns, which `startSpan`'s auto-end misses. `startInactiveSpan` leaves the return value untouched (no thenable probing). #### Semantic Conventions Attributes Dropping the `OTEL_SEMCONV_STABILITY_OPT_IN` path means ioredis spans now **always** emit the legacy semantic-convention attributes (`db.system`, `db.statement`, `db.connection_string`, `net.peer.*`). Modernizing the semconv is deferred as a separate, breaking change. --- .oxlintrc.base.json | 3 +- .../suites/tracing/redis/scenario-ioredis.mjs | 3 + .../suites/tracing/redis/test.ts | 15 + .../redis/vendored/ioredis-instrumentation.ts | 342 ++++++++---------- .../tracing/redis/vendored/types.ts | 21 -- .../redis/ioredis-instrumentation.test.ts | 151 -------- 6 files changed, 163 insertions(+), 372 deletions(-) delete mode 100644 packages/node/test/integrations/tracing/redis/ioredis-instrumentation.test.ts diff --git a/.oxlintrc.base.json b/.oxlintrc.base.json index 1098efdb425a..6f65b4f097ae 100644 --- a/.oxlintrc.base.json +++ b/.oxlintrc.base.json @@ -156,7 +156,8 @@ "**/integrations/tracing/prisma/vendored/**/*.ts", "**/integrations/tracing/graphql/vendored/**/*.ts", "**/integrations/tracing/postgres/vendored/**/*.ts", - "**/integrations/tracing/fastify/vendored/**/*.ts" + "**/integrations/tracing/fastify/vendored/**/*.ts", + "**/integrations/tracing/redis/vendored/**/*.ts" ], "rules": { "typescript/no-explicit-any": "off", diff --git a/dev-packages/node-integration-tests/suites/tracing/redis/scenario-ioredis.mjs b/dev-packages/node-integration-tests/suites/tracing/redis/scenario-ioredis.mjs index c7b31c799f52..5973252d598d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis/scenario-ioredis.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/redis/scenario-ioredis.mjs @@ -14,6 +14,9 @@ async function run() { await redis.set('test-key', 'test-value'); await redis.get('test-key'); + + // a failing command should produce a span with an error status + await redis.incr('test-key').catch(() => {}); } finally { await redis.disconnect(); } diff --git a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts index 59a5b9ca929c..6ee6f537c1f5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts @@ -35,6 +35,21 @@ describe('redis auto instrumentation', () => { 'db.statement': 'get test-key', }), }), + // a failing command produces a span with an error status + expect.objectContaining({ + description: 'incr test-key', + op: 'db', + status: 'internal_error', + origin: 'auto.db.otel.redis', + data: expect.objectContaining({ + 'sentry.op': 'db', + 'sentry.origin': 'auto.db.otel.redis', + 'db.system': 'redis', + 'net.peer.name': 'localhost', + 'net.peer.port': 6379, + 'db.statement': 'incr test-key', + }), + }), ]), }; diff --git a/packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts b/packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts index e862e1f1cdd1..e7cdb4be2761 100644 --- a/packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts +++ b/packages/node/src/integrations/tracing/redis/vendored/ioredis-instrumentation.ts @@ -17,27 +17,19 @@ * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/instrumentation-ioredis-v0.62.0/packages/instrumentation-ioredis * - Upstream version: @opentelemetry/instrumentation-ioredis@0.62.0 * - Minor TypeScript adjustments for this repository's compiler settings + * - Refactored to use Sentry's span APIs instead of OpenTelemetry tracing APIs */ -/* eslint-disable -- vendored @opentelemetry/instrumentation-ioredis */ -import { context, diag, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api'; -import type { Span } from '@opentelemetry/api'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SpanKind } from '@opentelemetry/api'; +import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; +import type { Span, SpanAttributes } from '@sentry/core'; import { - InstrumentationBase, - InstrumentationNodeModuleDefinition, - isWrapped, - safeExecuteInTheMiddle, - SemconvStability, - semconvStabilityFromStr, -} from '@opentelemetry/instrumentation'; -import { - ATTR_DB_QUERY_TEXT, - ATTR_DB_SYSTEM_NAME, - ATTR_SERVER_ADDRESS, - ATTR_SERVER_PORT, -} from '@opentelemetry/semantic-conventions'; - + getActiveSpan, + SDK_VERSION, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SPAN_STATUS_ERROR, + startInactiveSpan, +} from '@sentry/core'; import { defaultDbStatementSerializer } from './redis-common'; import { ATTR_DB_CONNECTION_STRING, @@ -45,77 +37,78 @@ import { ATTR_DB_SYSTEM, ATTR_NET_PEER_NAME, ATTR_NET_PEER_PORT, - DB_SYSTEM_NAME_VALUE_REDIS, DB_SYSTEM_VALUE_REDIS, } from './semconv'; import type { IORedisInstrumentationConfig } from './types'; -const PACKAGE_NAME = '@opentelemetry/instrumentation-ioredis'; -const PACKAGE_VERSION = '0.62.0'; +const PACKAGE_NAME = '@sentry/instrumentation-ioredis'; +const ORIGIN = 'auto.db.otel.redis'; + +// ioredis >= 5.11.0 publishes via diagnostics_channel, which Sentry subscribes +// to separately, so this monkey-patching instrumentation only covers < 5.11.0. +const SUPPORTED_VERSIONS = ['>=2.0.0 <5.11.0']; + +// The raw imported `ioredis` module is either the CommonJS export or an ESM +// namespace wrapping it on `.default`. Typed shallowly since it is only used +// internally to reach the `Redis` prototype that holds the methods we patch. +type IORedisModule = { + default?: { prototype: RedisPrototype }; + prototype: RedisPrototype; + [Symbol.toStringTag]?: string; +}; + +interface RedisPrototype { + sendCommand: (...args: unknown[]) => unknown; + connect: (...args: unknown[]) => unknown; +} -// ---- utils ---- +// The `this` of the patched methods is a Redis client instance exposing its +// connection options. +interface RedisClient { + options: { host?: string; port?: number }; +} + +// The in-flight command object ioredis passes to `sendCommand`. We swap its +// `resolve`/`reject` so the span ends when the command settles. +interface RedisCommand { + name: string; + args: Array; + resolve: (result: unknown) => void; + reject: (err: Error) => void; +} function endSpan(span: Span, err: Error | null | undefined): void { if (err) { - span.recordException(err); - span.setStatus({ - code: SpanStatusCode.ERROR, - message: err.message, - }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: err.message }); } span.end(); } -// ---- IORedisInstrumentation ---- - -const DEFAULT_CONFIG: IORedisInstrumentationConfig = { - requireParentSpan: true, -}; - export class IORedisInstrumentation extends InstrumentationBase { - _netSemconvStability!: SemconvStability; - _dbSemconvStability!: SemconvStability; - - constructor(config: IORedisInstrumentationConfig = {}) { - super(PACKAGE_NAME, PACKAGE_VERSION, { ...DEFAULT_CONFIG, ...config }); - this._setSemconvStabilityFromEnv(); + public constructor(config: IORedisInstrumentationConfig = {}) { + super(PACKAGE_NAME, SDK_VERSION, config); } - _setSemconvStabilityFromEnv(): void { - this._netSemconvStability = semconvStabilityFromStr('http', process.env['OTEL_SEMCONV_STABILITY_OPT_IN']); - this._dbSemconvStability = semconvStabilityFromStr('database', process.env['OTEL_SEMCONV_STABILITY_OPT_IN']); - } - - override setConfig(config: IORedisInstrumentationConfig = {}): void { - super.setConfig({ ...DEFAULT_CONFIG, ...config }); - } - - init() { + protected init(): InstrumentationNodeModuleDefinition[] { return [ new InstrumentationNodeModuleDefinition( 'ioredis', - ['>=2.0.0 <5.11.0'], - (module: any, moduleVersion?: string) => { - const moduleExports = - module[Symbol.toStringTag] === 'Module' - ? module.default // ESM - : module; // CommonJS + SUPPORTED_VERSIONS, + (module: IORedisModule) => { + const moduleExports = module[Symbol.toStringTag] === 'Module' && module.default ? module.default : module; if (isWrapped(moduleExports.prototype.sendCommand)) { this._unwrap(moduleExports.prototype, 'sendCommand'); } - this._wrap(moduleExports.prototype, 'sendCommand', this._patchSendCommand(moduleVersion)); + this._wrap(moduleExports.prototype, 'sendCommand', this._patchSendCommand()); if (isWrapped(moduleExports.prototype.connect)) { this._unwrap(moduleExports.prototype, 'connect'); } this._wrap(moduleExports.prototype, 'connect', this._patchConnection()); return module; }, - (module: any) => { + (module: IORedisModule | undefined) => { if (module === undefined) return; - const moduleExports = - module[Symbol.toStringTag] === 'Module' - ? module.default // ESM - : module; // CommonJS + const moduleExports = module[Symbol.toStringTag] === 'Module' && module.default ? module.default : module; this._unwrap(moduleExports.prototype, 'sendCommand'); this._unwrap(moduleExports.prototype, 'connect'); }, @@ -123,152 +116,103 @@ export class IORedisInstrumentation extends InstrumentationBase { - return this._traceSendCommand(original, moduleVersion); + private _patchSendCommand() { + const instrumentation = this; + return (original: (...args: unknown[]) => unknown) => { + return function (this: RedisClient, ...args: unknown[]): unknown { + const cmd = args[0] as RedisCommand | undefined; + // ioredis only creates a span when there is an active parent span + // (the upstream `requireParentSpan` default, which Sentry never overrides). + if (args.length < 1 || typeof cmd !== 'object' || !getActiveSpan()) { + return original.apply(this, args); + } + + const { host, port } = this.options; + const attributes: SpanAttributes = { + [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_REDIS, + [ATTR_DB_STATEMENT]: defaultDbStatementSerializer(cmd.name, cmd.args), + [ATTR_DB_CONNECTION_STRING]: `redis://${host}:${port}`, + [ATTR_NET_PEER_NAME]: host, + [ATTR_NET_PEER_PORT]: port, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + }; + + const span = startInactiveSpan({ name: cmd.name, kind: SpanKind.CLIENT, attributes }); + + try { + const result = original.apply(this, args); + const origResolve = cmd.resolve; + cmd.resolve = function (response: unknown): void { + instrumentation._callResponseHook(span, cmd, response); + endSpan(span, null); + origResolve(response); + }; + const origReject = cmd.reject; + cmd.reject = function (err: Error): void { + endSpan(span, err); + origReject(err); + }; + return result; + } catch (error) { + endSpan(span, error as Error); + throw error; + } + }; }; } private _patchConnection() { - return (original: Function) => { - return this._traceConnection(original); - }; - } + return (original: (...args: unknown[]) => unknown) => { + return function (this: RedisClient, ...args: unknown[]): unknown { + if (!getActiveSpan()) { + return original.apply(this, args); + } - private _traceSendCommand(original: Function, moduleVersion?: string) { - const instrumentation = this; - return function (this: any, cmd: any) { - if (arguments.length < 1 || typeof cmd !== 'object') { - return original.apply(this, arguments); - } - const config = instrumentation.getConfig(); - const dbStatementSerializer = config.dbStatementSerializer || defaultDbStatementSerializer; - const hasNoParentSpan = trace.getSpan(context.active()) === undefined; - if (config.requireParentSpan === true && hasNoParentSpan) { - return original.apply(this, arguments); - } - const attributes: Record = {}; - const { host, port } = this.options; - const dbQueryText = dbStatementSerializer(cmd.name, cmd.args); - if (instrumentation._dbSemconvStability & SemconvStability.OLD) { - attributes[ATTR_DB_SYSTEM] = DB_SYSTEM_VALUE_REDIS; - attributes[ATTR_DB_STATEMENT] = dbQueryText; - attributes[ATTR_DB_CONNECTION_STRING] = `redis://${host}:${port}`; - } - if (instrumentation._dbSemconvStability & SemconvStability.STABLE) { - attributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_REDIS; - attributes[ATTR_DB_QUERY_TEXT] = dbQueryText; - } - if (instrumentation._netSemconvStability & SemconvStability.OLD) { - attributes[ATTR_NET_PEER_NAME] = host; - attributes[ATTR_NET_PEER_PORT] = port; - } - if (instrumentation._netSemconvStability & SemconvStability.STABLE) { - attributes[ATTR_SERVER_ADDRESS] = host; - attributes[ATTR_SERVER_PORT] = port; - } - attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.db.otel.redis'; - const span = instrumentation.tracer.startSpan(cmd.name, { - kind: SpanKind.CLIENT, - attributes, - }); - const { requestHook } = config; - if (requestHook) { - safeExecuteInTheMiddle( - () => - requestHook(span, { - moduleVersion, - cmdName: cmd.name, - cmdArgs: cmd.args, - }), - (e: Error | undefined) => { - if (e) { - diag.error('ioredis instrumentation: request hook failed', e); - } - }, - true, - ); - } - try { - const result = original.apply(this, arguments); - const origResolve = cmd.resolve; - cmd.resolve = function (result: unknown) { - safeExecuteInTheMiddle( - () => config.responseHook?.(span, cmd.name, cmd.args, result), - (e: Error | undefined) => { - if (e) { - diag.error('ioredis instrumentation: response hook failed', e); - } - }, - true, - ); - endSpan(span, null); - origResolve(result); - }; - const origReject = cmd.reject; - cmd.reject = function (err: Error) { - endSpan(span, err); - origReject(err); + const { host, port } = this.options; + const attributes: SpanAttributes = { + [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_REDIS, + [ATTR_DB_STATEMENT]: 'connect', + [ATTR_DB_CONNECTION_STRING]: `redis://${host}:${port}`, + [ATTR_NET_PEER_NAME]: host, + [ATTR_NET_PEER_PORT]: port, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, }; - return result; - } catch (error) { - endSpan(span, error as Error); - throw error; - } - }; - } - private _traceConnection(original: Function) { - const instrumentation = this; - return function (this: any) { - const hasNoParentSpan = trace.getSpan(context.active()) === undefined; - if (instrumentation.getConfig().requireParentSpan === true && hasNoParentSpan) { - return original.apply(this, arguments); - } - const attributes: Record = {}; - const { host, port } = this.options; - if (instrumentation._dbSemconvStability & SemconvStability.OLD) { - attributes[ATTR_DB_SYSTEM] = DB_SYSTEM_VALUE_REDIS; - attributes[ATTR_DB_STATEMENT] = 'connect'; - attributes[ATTR_DB_CONNECTION_STRING] = `redis://${host}:${port}`; - } - if (instrumentation._dbSemconvStability & SemconvStability.STABLE) { - attributes[ATTR_DB_SYSTEM_NAME] = DB_SYSTEM_NAME_VALUE_REDIS; - attributes[ATTR_DB_QUERY_TEXT] = 'connect'; - } - if (instrumentation._netSemconvStability & SemconvStability.OLD) { - attributes[ATTR_NET_PEER_NAME] = host; - attributes[ATTR_NET_PEER_PORT] = port; - } - if (instrumentation._netSemconvStability & SemconvStability.STABLE) { - attributes[ATTR_SERVER_ADDRESS] = host; - attributes[ATTR_SERVER_PORT] = port; - } - attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.db.otel.redis'; - const span = instrumentation.tracer.startSpan('connect', { - kind: SpanKind.CLIENT, - attributes, - }); - try { - const result = original.apply(this, arguments); - if (typeof result?.then === 'function') { - return result.then( - (value: unknown) => { - endSpan(span, null); - return value; - }, - (error: Error) => { - endSpan(span, error); - return Promise.reject(error); - }, - ); + const span = startInactiveSpan({ name: 'connect', kind: SpanKind.CLIENT, attributes }); + + try { + const result = original.apply(this, args) as Promise | undefined; + if (result instanceof Promise) { + return result.then( + (value: unknown) => { + endSpan(span, null); + return value; + }, + (error: Error) => { + endSpan(span, error); + return Promise.reject(error); + }, + ); + } + endSpan(span, null); + return result; + } catch (error) { + endSpan(span, error as Error); + throw error; } - endSpan(span, null); - return result; - } catch (error) { - endSpan(span, error as Error); - throw error; - } + }; }; } + + private _callResponseHook(span: Span, cmd: RedisCommand, response: unknown): void { + const { responseHook } = this.getConfig(); + if (!responseHook) { + return; + } + try { + responseHook(span, cmd.name, cmd.args, response); + } catch { + // ignore errors thrown from the user-provided response hook + } + } } diff --git a/packages/node/src/integrations/tracing/redis/vendored/types.ts b/packages/node/src/integrations/tracing/redis/vendored/types.ts index 24b3817857d5..f4dac8fdea8b 100644 --- a/packages/node/src/integrations/tracing/redis/vendored/types.ts +++ b/packages/node/src/integrations/tracing/redis/vendored/types.ts @@ -58,21 +58,6 @@ export interface RedisInstrumentationConfig extends InstrumentationConfig { export type CommandArgs = Array; -/** - * Function that can be used to serialize db.statement tag for ioredis - */ -export type IORedisDbStatementSerializer = (cmdName: string, cmdArgs: CommandArgs) => string; - -export interface IORedisRequestHookInformation { - moduleVersion?: string; - cmdName: string; - cmdArgs: CommandArgs; -} - -export interface RedisRequestCustomAttributeFunction { - (span: Span, requestInfo: IORedisRequestHookInformation): void; -} - /** * Function that can be used to add custom attributes to span on response from redis server (ioredis) */ @@ -81,12 +66,6 @@ export interface IORedisResponseCustomAttributeFunction { } export interface IORedisInstrumentationConfig extends InstrumentationConfig { - /** Custom serializer function for the db.statement tag */ - dbStatementSerializer?: IORedisDbStatementSerializer; - /** Function for adding custom attributes on db request */ - requestHook?: RedisRequestCustomAttributeFunction; /** Function for adding custom attributes on db response */ responseHook?: IORedisResponseCustomAttributeFunction; - /** Require parent to create ioredis span, default when unset is true */ - requireParentSpan?: boolean; } diff --git a/packages/node/test/integrations/tracing/redis/ioredis-instrumentation.test.ts b/packages/node/test/integrations/tracing/redis/ioredis-instrumentation.test.ts deleted file mode 100644 index 3ef0158c2526..000000000000 --- a/packages/node/test/integrations/tracing/redis/ioredis-instrumentation.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Tests ported from @opentelemetry/instrumentation-ioredis@0.62.0 - * Original source: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-ioredis - * Licensed under the Apache License, Version 2.0 - */ - -import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -import { IORedisInstrumentation } from '../../../../src/integrations/tracing/redis/vendored/ioredis-instrumentation'; - -const memoryExporter = new InMemorySpanExporter(); -const provider = new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(memoryExporter)] }); - -describe('IORedisInstrumentation', () => { - let instrumentation: IORedisInstrumentation; - - beforeEach(() => { - instrumentation = new IORedisInstrumentation(); - instrumentation.setTracerProvider(provider); - memoryExporter.reset(); - }); - - afterEach(() => { - instrumentation.disable(); - vi.restoreAllMocks(); - }); - - describe('constructor', () => { - it('should create an instance with default config (requireParentSpan = true)', () => { - const inst = new IORedisInstrumentation(); - expect(inst).toBeInstanceOf(IORedisInstrumentation); - expect(inst.getConfig().requireParentSpan).toBe(true); - }); - - it('should create an instance with custom config', () => { - const inst = new IORedisInstrumentation({ requireParentSpan: false }); - expect(inst.getConfig().requireParentSpan).toBe(false); - }); - }); - - describe('setConfig', () => { - it('should preserve default requireParentSpan = true when config is empty', () => { - instrumentation.setConfig({}); - expect(instrumentation.getConfig().requireParentSpan).toBe(true); - }); - - it('should allow overriding requireParentSpan', () => { - instrumentation.setConfig({ requireParentSpan: false }); - expect(instrumentation.getConfig().requireParentSpan).toBe(false); - }); - }); - - describe('init', () => { - it('should return module definitions for ioredis', () => { - const defs = instrumentation.init(); - expect(Array.isArray(defs)).toBe(true); - expect(defs).toHaveLength(1); - expect(defs[0]!.name).toBe('ioredis'); - }); - - it('should support ioredis versions >=2.0.0 <5.11.0', () => { - const defs = instrumentation.init(); - const supportedVersions = defs[0]!.supportedVersions; - expect(supportedVersions).toContain('>=2.0.0 <5.11.0'); - }); - }); - - describe('_patchSendCommand', () => { - it('should skip tracing when no parent span and requireParentSpan is true', () => { - instrumentation.setConfig({ requireParentSpan: true }); - const original = vi.fn().mockReturnValue(Promise.resolve('OK')); - - const patchFn = (instrumentation as any)._patchSendCommand(); - const patched = patchFn(original); - - const fakeThis = { - options: { host: 'localhost', port: 6379 }, - }; - const fakeCmd = { - name: 'get', - args: ['mykey'], - resolve: vi.fn(), - reject: vi.fn(), - }; - - patched.call(fakeThis, fakeCmd); - - expect(original).toHaveBeenCalled(); - expect(memoryExporter.getFinishedSpans()).toHaveLength(0); - }); - - it('should not trace when called with less than 1 argument', () => { - const original = vi.fn().mockReturnValue(undefined); - const patchFn = (instrumentation as any)._patchSendCommand(); - const patched = patchFn(original); - - const fakeThis = { options: { host: 'localhost', port: 6379 } }; - - patched.call(fakeThis); - - expect(original).toHaveBeenCalled(); - expect(memoryExporter.getFinishedSpans()).toHaveLength(0); - }); - - it('should not trace when cmd is not an object', () => { - const original = vi.fn().mockReturnValue(undefined); - const patchFn = (instrumentation as any)._patchSendCommand(); - const patched = patchFn(original); - - const fakeThis = { options: { host: 'localhost', port: 6379 } }; - - patched.call(fakeThis, 'not-an-object'); - - expect(original).toHaveBeenCalled(); - expect(memoryExporter.getFinishedSpans()).toHaveLength(0); - }); - }); - - describe('_patchConnection', () => { - it('should skip tracing when no parent span and requireParentSpan is true', () => { - instrumentation.setConfig({ requireParentSpan: true }); - const original = vi.fn().mockReturnValue({ connected: true }); - - const patchFn = (instrumentation as any)._patchConnection(); - const patched = patchFn(original); - - const fakeThis = { options: { host: 'localhost', port: 6379 } }; - - patched.call(fakeThis); - - expect(original).toHaveBeenCalled(); - expect(memoryExporter.getFinishedSpans()).toHaveLength(0); - }); - }); - - describe('semconv stability', () => { - it('should initialize semconv stability from env', () => { - const inst = new IORedisInstrumentation(); - expect((inst as any)._netSemconvStability).toBeDefined(); - expect((inst as any)._dbSemconvStability).toBeDefined(); - }); - - it('should allow resetting semconv stability', () => { - const inst = new IORedisInstrumentation(); - const originalNet = (inst as any)._netSemconvStability; - inst._setSemconvStabilityFromEnv(); - expect((inst as any)._netSemconvStability).toBe(originalNet); - }); - }); -});