diff --git a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/instrument.mjs index c0a1998369a5..db5276b8c536 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts index e6866955ca5c..d854e0386637 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ioredis-dc/test.ts @@ -1,6 +1,28 @@ -import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; -import type { SerializedStreamedSpanContainer } from '@sentry/core'; -import { afterAll, describe, expect } from 'vitest'; +import { + CACHE_HIT, + CACHE_ITEM_SIZE, + CACHE_KEY, + CACHE_OPERATION, + DB_OPERATION_NAME, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + NETWORK_PEER_ADDRESS, + NETWORK_PEER_PORT, + SENTRY_ENVIRONMENT, + SENTRY_IS_LOCALHOST, + SENTRY_OP, + SENTRY_ORIGIN, + SENTRY_RELEASE, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_SEGMENT_ID, + SENTRY_SEGMENT_NAME, + SENTRY_TRACE_LIFECYCLE, + SERVER_ADDRESS, + SERVER_PORT, +} from '@sentry/conventions/attributes'; +import { CACHE_GET, CACHE_PUT, type CACHE_REMOVE, DB_QUERY } from '@sentry/conventions/op'; +import { afterAll, expect } from 'vitest'; import { EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -12,228 +34,123 @@ describeWithDockerCompose( cleanupChildProcesses(); }); - const EXPECTED_TRANSACTION = { - transaction: 'Test Span IORedis 5.11 DC', - spans: expect.arrayContaining([ - expect.objectContaining({ - op: 'db.query', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.op': 'db.query', - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.system.name': 'redis', - 'db.query.text': 'set dc-test-key ?', - }), - }), - expect.objectContaining({ - description: 'dc-cache:test-key', - op: 'cache.put', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.query.text': 'set dc-cache:test-key ?', - 'cache.key': ['dc-cache:test-key'], - 'cache.item_size': 2, - }), - }), - expect.objectContaining({ - description: 'dc-cache:test-key-ex', - op: 'cache.put', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.query.text': 'set dc-cache:test-key-ex ? ? ?', - 'cache.key': ['dc-cache:test-key-ex'], - 'cache.item_size': 2, - }), - }), - expect.objectContaining({ - op: 'db.query', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.op': 'db.query', - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.system.name': 'redis', - 'db.query.text': 'get dc-test-key', - }), - }), - expect.objectContaining({ - description: 'dc-cache:test-key', - op: 'cache.get', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.query.text': 'get dc-cache:test-key', - 'cache.hit': true, - 'cache.key': ['dc-cache:test-key'], - 'cache.item_size': 10, - }), - }), - expect.objectContaining({ - description: 'dc-cache:unavailable-data', - op: 'cache.get', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.query.text': 'get dc-cache:unavailable-data', - 'cache.hit': false, - 'cache.key': ['dc-cache:unavailable-data'], - }), - }), - expect.objectContaining({ - op: 'db.query', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.op': 'db.query', - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.system.name': 'redis', - 'db.query.text': 'mget ? ? ?', - }), - }), - ]), - }; - - const EXPECTED_CONNECT = { - transaction: 'redis-connect', - }; + const ORIGIN = 'auto.db.redis.diagnostic_channel'; + const SEGMENT_NAME = 'Test Span IORedis 5.11 DC'; + const HOST = '127.0.0.1'; + const PORT = 6382; - createEsmAndCjsTests(__dirname, 'scenario-ioredis-5-11.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('creates spans for ioredis v5.11 commands via diagnostics_channel', { timeout: 75_000 }, async () => { - await createTestRunner() - .expect({ transaction: EXPECTED_CONNECT }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .start() - .completed(); - }); + const streamAttribute = (value: unknown): { type: string; value: unknown } => ({ + type: Array.isArray(value) ? 'array' : Number.isInteger(value) ? 'integer' : typeof value, + value, }); - // The same commands as above, asserted on the streamed span container. With span streaming the - // db spans are named `{db.operation.name} {server.address}:{server.port}` instead of - // `redis-{command}`. ioredis reports its commands lowercase, so the name follows suit. - describe('streamed', () => { - const ORIGIN = 'auto.db.redis.diagnostic_channel'; - const SEGMENT_NAME = 'Test Span IORedis 5.11 DC'; - const HOST = '127.0.0.1'; - const PORT = 6382; - - const streamAttribute = (value: unknown): { type: string; value: unknown } => ({ - type: Array.isArray(value) ? 'array' : Number.isInteger(value) ? 'integer' : typeof value, - value, - }); - - // Streamed spans carry `{ type, value }` attribute pairs; the expectations below are written - // as plain values and wrapped here. - const streamAttributes = (values: Record): Record => - Object.fromEntries(Object.entries(values).map(([key, value]) => [key, streamAttribute(value)])); + // Streamed spans carry `{ type, value }` attribute pairs; the expectations below are written + // as plain values and wrapped here. + const streamAttributes = (values: Record): Record => + Object.fromEntries(Object.entries(values).map(([key, value]) => [key, streamAttribute(value)])); - function streamedSpan(name: string, op: string, attributes: Record): unknown { - return { - name, - attributes: { - ...streamAttributes({ - 'db.system.name': 'redis', - 'sentry.environment': 'production', - 'sentry.op': op, - 'sentry.origin': ORIGIN, - 'sentry.release': '1.0', - 'sentry.sdk.name': EXPECTED_SDK_NAME, - 'sentry.segment.name': SEGMENT_NAME, - 'server.address': HOST, - 'server.port': PORT, - [SENTRY_TRACE_LIFECYCLE]: 'stream', - ...attributes, - }), - 'sentry.sdk.version': { type: 'string', value: expect.any(String) }, - 'sentry.segment.id': { type: 'string', value: expect.stringMatching(/^[\da-f]{16}$/) }, - 'sentry.is_localhost': { type: 'boolean', value: false }, - }, - end_timestamp: expect.any(Number), - is_segment: false, - parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - start_timestamp: expect.any(Number), - status: 'ok', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - }; - } + function streamedSpan(name: string, op: string, attributes: Record): unknown { + return { + name, + attributes: { + ...streamAttributes({ + [DB_SYSTEM_NAME]: 'redis', + [SENTRY_ENVIRONMENT]: 'production', + [SENTRY_OP]: op, + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_RELEASE]: '1.0', + [SENTRY_SDK_NAME]: EXPECTED_SDK_NAME, + [SENTRY_SEGMENT_NAME]: SEGMENT_NAME, + [SERVER_ADDRESS]: HOST, + [SERVER_PORT]: PORT, + [SENTRY_TRACE_LIFECYCLE]: 'stream', + ...attributes, + }), + [SENTRY_SDK_VERSION]: { type: 'string', value: expect.any(String) }, + [SENTRY_SEGMENT_ID]: { type: 'string', value: expect.stringMatching(/^[\da-f]{16}$/) }, + [SENTRY_IS_LOCALHOST]: { type: 'boolean', value: false }, + }, + end_timestamp: expect.any(Number), + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + status: 'ok', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + }; + } - const PEER = { 'network.peer.address': HOST, 'network.peer.port': PORT }; + const PEER = { [NETWORK_PEER_ADDRESS]: HOST, [NETWORK_PEER_PORT]: PORT }; - // A cache span is a db span the cache hook took over: it is renamed to its cache operation - // and reports the connection it inherited as peer attributes too. - const cacheSpan = ( - op: 'cache.get' | 'cache.put' | 'cache.remove', - attributes: Record, - ): unknown => streamedSpan(op, op, { ...PEER, 'cache.operation': op.slice('cache.'.length), ...attributes }); + // A cache span is a db span the cache hook took over: it is renamed to its cache operation + // and reports the connection it inherited as peer attributes too. + const cacheSpan = ( + op: typeof CACHE_GET | typeof CACHE_PUT | typeof CACHE_REMOVE, + attributes: Record, + ): unknown => streamedSpan(op, op, { ...PEER, [CACHE_OPERATION]: op.slice('cache.'.length), ...attributes }); - createEsmAndCjsTests(__dirname, 'scenario-ioredis-5-11.mjs', 'instrument.mjs', (createTestRunner, test) => { - test( - 'creates streamed spans for ioredis v5.11 commands via diagnostics_channel', - { timeout: 75_000 }, - async () => { - await createTestRunner() - .withEnv({ STREAMED: 'true' }) - .expect({ - span: (container: SerializedStreamedSpanContainer) => { - // The connect span opens its own segment but shares the trace with the test span, - // so both segments arrive in the same container. - expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ - 'redis-connect', - SEGMENT_NAME, - ]); + createEsmAndCjsTests(__dirname, 'scenario-ioredis-5-11.mjs', 'instrument.mjs', (createTestRunner, test) => { + test('creates spans for ioredis v5.11 commands via diagnostics_channel', { timeout: 75_000 }, async () => { + await createTestRunner() + .expect({ + span: container => { + // The connect span opens its own segment but shares the trace with the test span, + // so both segments arrive in the same container. + expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ + 'redis-connect', + SEGMENT_NAME, + ]); - // ioredis' own handshake commands (`client SETINFO`, `info`) are emitted on the - // channel too, but belong to the connect segment — the test span's children are the - // commands the scenario issues. - const spans = container.items.filter( - item => !item.is_segment && item.attributes['sentry.segment.name']?.value === SEGMENT_NAME, - ); + // ioredis' own handshake commands (`client SETINFO`, `info`) are emitted on the + // channel too, but belong to the connect segment — the test span's children are the + // commands the scenario issues. + const spans = container.items.filter( + item => !item.is_segment && item.attributes[SENTRY_SEGMENT_NAME]?.value === SEGMENT_NAME, + ); - expect(spans).toEqual([ - streamedSpan(`set ${HOST}:${PORT}`, 'db.query', { - 'db.operation.name': 'set', - 'db.query.text': 'set dc-test-key ?', - }), - cacheSpan('cache.put', { - 'db.operation.name': 'set', - 'db.query.text': 'set dc-cache:test-key ?', - 'cache.key': ['dc-cache:test-key'], - 'cache.item_size': 2, - }), - cacheSpan('cache.put', { - 'db.operation.name': 'set', - 'db.query.text': 'set dc-cache:test-key-ex ? ? ?', - 'cache.key': ['dc-cache:test-key-ex'], - 'cache.item_size': 2, - }), - streamedSpan(`get ${HOST}:${PORT}`, 'db.query', { - 'db.operation.name': 'get', - 'db.query.text': 'get dc-test-key', - }), - cacheSpan('cache.get', { - 'db.operation.name': 'get', - 'db.query.text': 'get dc-cache:test-key', - 'cache.key': ['dc-cache:test-key'], - 'cache.hit': true, - 'cache.item_size': 10, - }), - cacheSpan('cache.get', { - 'db.operation.name': 'get', - 'db.query.text': 'get dc-cache:unavailable-data', - 'cache.key': ['dc-cache:unavailable-data'], - 'cache.hit': false, - }), - streamedSpan(`mget ${HOST}:${PORT}`, 'db.query', { - 'db.operation.name': 'mget', - 'db.query.text': 'mget ? ? ?', - }), - ]); - }, - }) - .start() - .completed(); - }, - ); + expect(spans).toEqual([ + streamedSpan(`set ${HOST}:${PORT}`, DB_QUERY, { + [DB_OPERATION_NAME]: 'set', + [DB_QUERY_TEXT]: 'set dc-test-key ?', + }), + cacheSpan(CACHE_PUT, { + [DB_OPERATION_NAME]: 'set', + [DB_QUERY_TEXT]: 'set dc-cache:test-key ?', + [CACHE_KEY]: ['dc-cache:test-key'], + [CACHE_ITEM_SIZE]: 2, + }), + cacheSpan(CACHE_PUT, { + [DB_OPERATION_NAME]: 'set', + [DB_QUERY_TEXT]: 'set dc-cache:test-key-ex ? ? ?', + [CACHE_KEY]: ['dc-cache:test-key-ex'], + [CACHE_ITEM_SIZE]: 2, + }), + streamedSpan(`get ${HOST}:${PORT}`, DB_QUERY, { + [DB_OPERATION_NAME]: 'get', + [DB_QUERY_TEXT]: 'get dc-test-key', + }), + cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'get', + [DB_QUERY_TEXT]: 'get dc-cache:test-key', + [CACHE_KEY]: ['dc-cache:test-key'], + [CACHE_HIT]: true, + [CACHE_ITEM_SIZE]: 10, + }), + cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'get', + [DB_QUERY_TEXT]: 'get dc-cache:unavailable-data', + [CACHE_KEY]: ['dc-cache:unavailable-data'], + [CACHE_HIT]: false, + }), + streamedSpan(`mget ${HOST}:${PORT}`, DB_QUERY, { + [DB_OPERATION_NAME]: 'mget', + [DB_QUERY_TEXT]: 'mget ? ? ?', + }), + ]); + }, + }) + .start() + .completed(); }); }); }, diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/instrument-redis-4.mjs b/dev-packages/node-integration-tests/suites/tracing/redis-cache/instrument-redis-4.mjs index 9b24191fd0a1..df9d0ca876df 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/instrument-redis-4.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/instrument-redis-4.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/instrument-redis-5.mjs b/dev-packages/node-integration-tests/suites/tracing/redis-cache/instrument-redis-5.mjs index 58f4a0875acd..285078fd57e7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/instrument-redis-5.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/instrument-redis-5.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts index 2e8de3443a55..a6f3271d2356 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts @@ -1,4 +1,31 @@ -import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; +import { + CACHE_HIT, + CACHE_ITEM_SIZE, + CACHE_KEY, + CACHE_OPERATION, + DB_OPERATION_BATCH_SIZE, + DB_OPERATION_NAME, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + ERROR_TYPE, + NETWORK_PEER_ADDRESS, + NETWORK_PEER_PORT, + SENTRY_ENVIRONMENT, + SENTRY_IS_LOCALHOST, + SENTRY_KIND, + SENTRY_OP, + SENTRY_ORIGIN, + SENTRY_RELEASE, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_SEGMENT_ID, + SENTRY_SEGMENT_NAME, + SENTRY_STATUS_MESSAGE, + SENTRY_TRACE_LIFECYCLE, + SERVER_ADDRESS, + SERVER_PORT, +} from '@sentry/conventions/attributes'; +import { CACHE_GET, CACHE_PUT, CACHE_REMOVE, DB_QUERY } from '@sentry/conventions/op'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { EXPECTED_SDK_NAME } from '../../../utils'; @@ -10,45 +37,7 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory }); const redisOrigin = 'auto.db.redis'; - const redisSpanOp = 'db.query'; - - describe('ioredis non-cache keys', () => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Span', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'set test-key [1 other arguments]', - op: redisSpanOp, - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.op': redisSpanOp, - 'db.system.name': 'redis', - 'server.address': 'localhost', - 'server.port': 6383, - 'db.query.text': 'set test-key [1 other arguments]', - }), - }), - expect.objectContaining({ - description: 'get test-key', - op: redisSpanOp, - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.op': redisSpanOp, - 'db.system.name': 'redis', - 'server.address': 'localhost', - 'server.port': 6383, - 'db.query.text': 'get test-key', - }), - }), - ]), - }; - - createEsmAndCjsTests(__dirname, 'scenario-ioredis.mjs', 'instrument-ioredis.mjs', (createTestRunner, test) => { - test('should not add cache spans when key is not prefixed', { timeout: 60_000 }, async () => { - await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); - }); - }); - }); + const redisSpanOp = DB_QUERY; describe('ioredis cache keys', () => { const EXPECTED_TRANSACTION = { @@ -57,7 +46,7 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory // SET expect.objectContaining({ description: 'ioredis-cache:test-key', - op: 'cache.put', + op: CACHE_PUT, origin: redisOrigin, data: expect.objectContaining({ 'sentry.origin': redisOrigin, @@ -72,7 +61,7 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory // SET (with EX) expect.objectContaining({ description: 'ioredis-cache:test-key-set-EX', - op: 'cache.put', + op: CACHE_PUT, origin: redisOrigin, data: expect.objectContaining({ 'sentry.origin': redisOrigin, @@ -87,7 +76,7 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory // SETEX expect.objectContaining({ description: 'ioredis-cache:test-key-setex', - op: 'cache.put', + op: CACHE_PUT, origin: redisOrigin, data: expect.objectContaining({ 'sentry.origin': redisOrigin, @@ -102,7 +91,7 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory // GET expect.objectContaining({ description: 'ioredis-cache:test-key', - op: 'cache.get', + op: CACHE_GET, origin: redisOrigin, data: expect.objectContaining({ 'sentry.origin': redisOrigin, @@ -118,7 +107,7 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory // GET (unavailable - no cache hit) expect.objectContaining({ description: 'ioredis-cache:unavailable-data', - op: 'cache.get', + op: CACHE_GET, origin: redisOrigin, data: expect.objectContaining({ 'sentry.origin': redisOrigin, @@ -133,7 +122,7 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory // MGET expect.objectContaining({ description: 'test-key, ioredis-cache:test-key, ioredis-cache:unavailable-data', - op: 'cache.get', + op: CACHE_GET, origin: redisOrigin, data: expect.objectContaining({ 'sentry.origin': redisOrigin, @@ -148,7 +137,7 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory // DEL expect.objectContaining({ description: 'ioredis-cache:test-key', - op: 'cache.remove', + op: CACHE_REMOVE, origin: redisOrigin, data: expect.objectContaining({ 'sentry.origin': redisOrigin, @@ -162,533 +151,14 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory ]), }; - // Same commands as above, but streamed: the cache key is gone from the span name and only - // `cache.key` still holds it. - const EXPECTED_STREAMED_SPANS = expect.arrayContaining([ - // SET - expect.objectContaining({ - name: 'cache.put', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.origin': { type: 'string', value: redisOrigin }, - 'sentry.op': { type: 'string', value: 'cache.put' }, - 'db.query.text': { type: 'string', value: 'set ioredis-cache:test-key [1 other arguments]' }, - 'cache.operation': { type: 'string', value: 'put' }, - 'cache.key': { type: 'array', value: ['ioredis-cache:test-key'] }, - 'cache.item_size': { type: 'integer', value: 2 }, - }), - }), - // SETEX - expect.objectContaining({ - name: 'cache.put', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'cache.put' }, - 'db.query.text': { type: 'string', value: 'setex ioredis-cache:test-key-setex [2 other arguments]' }, - 'cache.operation': { type: 'string', value: 'put' }, - 'cache.key': { type: 'array', value: ['ioredis-cache:test-key-setex'] }, - }), - }), - // GET - expect.objectContaining({ - name: 'cache.get', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.origin': { type: 'string', value: redisOrigin }, - 'sentry.op': { type: 'string', value: 'cache.get' }, - 'db.query.text': { type: 'string', value: 'get ioredis-cache:test-key' }, - 'cache.operation': { type: 'string', value: 'get' }, - 'cache.hit': { type: 'boolean', value: true }, - 'cache.key': { type: 'array', value: ['ioredis-cache:test-key'] }, - 'cache.item_size': { type: 'integer', value: 10 }, - }), - }), - // MGET - expect.objectContaining({ - name: 'cache.get', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'cache.get' }, - 'db.query.text': { type: 'string', value: 'mget [3 other arguments]' }, - 'cache.operation': { type: 'string', value: 'get' }, - 'cache.key': { - type: 'array', - value: ['test-key', 'ioredis-cache:test-key', 'ioredis-cache:unavailable-data'], - }, - }), - }), - // DEL - expect.objectContaining({ - name: 'cache.remove', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.origin': { type: 'string', value: redisOrigin }, - 'sentry.op': { type: 'string', value: 'cache.remove' }, - 'db.query.text': { type: 'string', value: 'del ioredis-cache:test-key' }, - 'cache.operation': { type: 'string', value: 'remove' }, - 'cache.key': { type: 'array', value: ['ioredis-cache:test-key'] }, - }), - }), - ]); - createEsmAndCjsTests(__dirname, 'scenario-ioredis.mjs', 'instrument-ioredis.mjs', (createTestRunner, test) => { test('should create cache spans for prefixed keys (ioredis)', { timeout: 60_000 }, async () => { await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); }); - - test('should name cache spans after the cache operation when streamed (ioredis)', async () => { - await createTestRunner() - .withEnv({ STREAMED: 'true' }) - .expect({ span: { items: EXPECTED_STREAMED_SPANS } }) - .start() - .completed(); - }); - }); - }); - - describe('redis-4 cache keys', () => { - const EXPECTED_REDIS_CONNECT = { - transaction: 'redis-connect', - }; - - const batchSpans = [ - expect.objectContaining({ - description: 'MULTI', - op: 'db.query', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.system.name': 'redis', - 'db.operation.batch.size': 2, - }), - }), - ]; - - const EXPECTED_TRANSACTION = { - transaction: 'Test Span Redis 4', - spans: expect.arrayContaining([ - // SET - expect.objectContaining({ - description: 'redis-cache:test-key', - op: 'cache.put', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'SET redis-cache:test-key [1 other arguments]', - 'cache.operation': 'put', - 'cache.key': ['redis-cache:test-key'], - 'cache.item_size': 2, - }), - }), - // SET (with EX) - expect.objectContaining({ - description: 'redis-cache:test-key-set-EX', - op: 'cache.put', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'SET redis-cache:test-key-set-EX [3 other arguments]', - 'cache.operation': 'put', - 'cache.key': ['redis-cache:test-key-set-EX'], - 'cache.item_size': 2, - }), - }), - // SETEX - expect.objectContaining({ - description: 'redis-cache:test-key-setex', - op: 'cache.put', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'SETEX redis-cache:test-key-setex [2 other arguments]', - 'cache.operation': 'put', - 'cache.key': ['redis-cache:test-key-setex'], - 'cache.item_size': 2, - }), - }), - // GET - expect.objectContaining({ - description: 'redis-cache:test-key', - op: 'cache.get', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'GET redis-cache:test-key', - 'cache.operation': 'get', - 'cache.hit': true, - 'cache.key': ['redis-cache:test-key'], - 'cache.item_size': 10, - }), - }), - // GET (unavailable - no cache hit) - expect.objectContaining({ - description: 'redis-cache:unavailable-data', - op: 'cache.get', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'GET redis-cache:unavailable-data', - 'cache.operation': 'get', - 'cache.hit': false, - 'cache.key': ['redis-cache:unavailable-data'], - }), - }), - // MGET - expect.objectContaining({ - description: 'redis-test-key, redis-cache:test-key, redis-cache:unavailable-data', - op: 'cache.get', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'MGET [3 other arguments]', - 'cache.operation': 'get', - 'cache.hit': true, - 'cache.key': ['redis-test-key', 'redis-cache:test-key', 'redis-cache:unavailable-data'], - }), - }), - // DEL - expect.objectContaining({ - description: 'redis-cache:test-key', - op: 'cache.remove', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'DEL redis-cache:test-key', - 'cache.operation': 'remove', - 'cache.key': ['redis-cache:test-key'], - }), - }), - ...batchSpans, - // a failing command produces a span with an error status - expect.objectContaining({ - description: 'INCR redis-test-key', - op: 'db.query', - status: 'internal_error', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.system.name': 'redis', - 'db.query.text': 'INCR redis-test-key', - }), - }), - ]), - }; - - const EXPECTED_STREAMED_SPANS = expect.arrayContaining([ - // SET - expect.objectContaining({ - name: 'cache.put', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.origin': { type: 'string', value: redisOrigin }, - 'sentry.op': { type: 'string', value: 'cache.put' }, - 'db.query.text': { type: 'string', value: 'SET redis-cache:test-key [1 other arguments]' }, - 'cache.operation': { type: 'string', value: 'put' }, - 'cache.key': { type: 'array', value: ['redis-cache:test-key'] }, - 'cache.item_size': { type: 'integer', value: 2 }, - }), - }), - // SETEX - expect.objectContaining({ - name: 'cache.put', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'cache.put' }, - 'db.query.text': { type: 'string', value: 'SETEX redis-cache:test-key-setex [2 other arguments]' }, - 'cache.operation': { type: 'string', value: 'put' }, - 'cache.key': { type: 'array', value: ['redis-cache:test-key-setex'] }, - }), - }), - // GET - expect.objectContaining({ - name: 'cache.get', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.origin': { type: 'string', value: redisOrigin }, - 'sentry.op': { type: 'string', value: 'cache.get' }, - 'db.query.text': { type: 'string', value: 'GET redis-cache:test-key' }, - 'cache.operation': { type: 'string', value: 'get' }, - 'cache.hit': { type: 'boolean', value: true }, - 'cache.key': { type: 'array', value: ['redis-cache:test-key'] }, - 'cache.item_size': { type: 'integer', value: 10 }, - }), - }), - // MGET - expect.objectContaining({ - name: 'cache.get', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'cache.get' }, - 'db.query.text': { type: 'string', value: 'MGET [3 other arguments]' }, - 'cache.operation': { type: 'string', value: 'get' }, - 'cache.key': { - type: 'array', - value: ['redis-test-key', 'redis-cache:test-key', 'redis-cache:unavailable-data'], - }, - }), - }), - // DEL - expect.objectContaining({ - name: 'cache.remove', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.origin': { type: 'string', value: redisOrigin }, - 'sentry.op': { type: 'string', value: 'cache.remove' }, - 'db.query.text': { type: 'string', value: 'DEL redis-cache:test-key' }, - 'cache.operation': { type: 'string', value: 'remove' }, - 'cache.key': { type: 'array', value: ['redis-cache:test-key'] }, - }), - }), - ]); - - createEsmAndCjsTests(__dirname, 'scenario-redis-4.mjs', 'instrument-redis-4.mjs', (createTestRunner, test) => { - test('should create cache spans for prefixed keys (redis-4)', async () => { - await createTestRunner() - .expect({ transaction: EXPECTED_REDIS_CONNECT }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .start() - .completed(); - }); - - test('should name cache spans after the cache operation when streamed (redis-4)', async () => { - await createTestRunner() - .withEnv({ STREAMED: 'true' }) - // The connect span opens its own segment. `unordered` keeps this expectation from - // depending on which segment the buffer happens to emit first. - .unordered() - .expect({ span: { items: EXPECTED_STREAMED_SPANS } }) - .start() - .completed(); - }); - }); - }); - - // node-redis 5.0-5.11 still flows through the vendored monkey-patch - // instrumentation (diagnostics_channel was only added in 5.12.0), so this - // exercises the `>=5.0.0 <5.12.0` branch of the vendored RedisInstrumentation. - describe('redis-5 cache keys', () => { - const EXPECTED_REDIS_CONNECT = { - transaction: 'redis-connect', - }; - - const batchSpans = [ - expect.objectContaining({ - description: 'MULTI', - op: 'db.query', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.system.name': 'redis', - 'db.operation.batch.size': 2, - }), - }), - ]; - - const EXPECTED_TRANSACTION = { - transaction: 'Test Span Redis 5', - spans: expect.arrayContaining([ - // SET - expect.objectContaining({ - description: 'redis-5-cache:test-key', - op: 'cache.put', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'SET redis-5-cache:test-key [1 other arguments]', - 'cache.operation': 'put', - 'cache.key': ['redis-5-cache:test-key'], - 'cache.item_size': 2, - }), - }), - // SET (with EX) - expect.objectContaining({ - description: 'redis-5-cache:test-key-set-EX', - op: 'cache.put', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'SET redis-5-cache:test-key-set-EX [3 other arguments]', - 'cache.operation': 'put', - 'cache.key': ['redis-5-cache:test-key-set-EX'], - 'cache.item_size': 2, - }), - }), - // SETEX - expect.objectContaining({ - description: 'redis-5-cache:test-key-setex', - op: 'cache.put', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'SETEX redis-5-cache:test-key-setex [2 other arguments]', - 'cache.operation': 'put', - 'cache.key': ['redis-5-cache:test-key-setex'], - 'cache.item_size': 2, - }), - }), - // GET - expect.objectContaining({ - description: 'redis-5-cache:test-key', - op: 'cache.get', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'GET redis-5-cache:test-key', - 'cache.operation': 'get', - 'cache.hit': true, - 'cache.key': ['redis-5-cache:test-key'], - 'cache.item_size': 10, - }), - }), - // GET (unavailable - no cache hit) - expect.objectContaining({ - description: 'redis-5-cache:unavailable-data', - op: 'cache.get', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'GET redis-5-cache:unavailable-data', - 'cache.operation': 'get', - 'cache.hit': false, - 'cache.key': ['redis-5-cache:unavailable-data'], - }), - }), - // MGET - expect.objectContaining({ - description: 'redis-5-test-key, redis-5-cache:test-key, redis-5-cache:unavailable-data', - op: 'cache.get', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'MGET [3 other arguments]', - 'cache.operation': 'get', - 'cache.hit': true, - 'cache.key': ['redis-5-test-key', 'redis-5-cache:test-key', 'redis-5-cache:unavailable-data'], - }), - }), - // DEL - expect.objectContaining({ - description: 'redis-5-cache:test-key', - op: 'cache.remove', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.query.text': 'DEL redis-5-cache:test-key', - 'cache.operation': 'remove', - 'cache.key': ['redis-5-cache:test-key'], - }), - }), - ...batchSpans, - // a failing command produces a span with an error status - expect.objectContaining({ - description: 'INCR redis-5-test-key', - op: 'db.query', - status: 'internal_error', - origin: redisOrigin, - data: expect.objectContaining({ - 'sentry.origin': redisOrigin, - 'db.system.name': 'redis', - 'db.query.text': 'INCR redis-5-test-key', - }), - }), - ]), - }; - - const EXPECTED_STREAMED_SPANS = expect.arrayContaining([ - // SET - expect.objectContaining({ - name: 'cache.put', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.origin': { type: 'string', value: redisOrigin }, - 'sentry.op': { type: 'string', value: 'cache.put' }, - 'db.query.text': { type: 'string', value: 'SET redis-5-cache:test-key [1 other arguments]' }, - 'cache.operation': { type: 'string', value: 'put' }, - 'cache.key': { type: 'array', value: ['redis-5-cache:test-key'] }, - 'cache.item_size': { type: 'integer', value: 2 }, - }), - }), - // SETEX - expect.objectContaining({ - name: 'cache.put', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'cache.put' }, - 'db.query.text': { type: 'string', value: 'SETEX redis-5-cache:test-key-setex [2 other arguments]' }, - 'cache.operation': { type: 'string', value: 'put' }, - 'cache.key': { type: 'array', value: ['redis-5-cache:test-key-setex'] }, - }), - }), - // GET - expect.objectContaining({ - name: 'cache.get', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.origin': { type: 'string', value: redisOrigin }, - 'sentry.op': { type: 'string', value: 'cache.get' }, - 'db.query.text': { type: 'string', value: 'GET redis-5-cache:test-key' }, - 'cache.operation': { type: 'string', value: 'get' }, - 'cache.hit': { type: 'boolean', value: true }, - 'cache.key': { type: 'array', value: ['redis-5-cache:test-key'] }, - 'cache.item_size': { type: 'integer', value: 10 }, - }), - }), - // MGET - expect.objectContaining({ - name: 'cache.get', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'cache.get' }, - 'db.query.text': { type: 'string', value: 'MGET [3 other arguments]' }, - 'cache.operation': { type: 'string', value: 'get' }, - 'cache.key': { - type: 'array', - value: ['redis-5-test-key', 'redis-5-cache:test-key', 'redis-5-cache:unavailable-data'], - }, - }), - }), - // DEL - expect.objectContaining({ - name: 'cache.remove', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.origin': { type: 'string', value: redisOrigin }, - 'sentry.op': { type: 'string', value: 'cache.remove' }, - 'db.query.text': { type: 'string', value: 'DEL redis-5-cache:test-key' }, - 'cache.operation': { type: 'string', value: 'remove' }, - 'cache.key': { type: 'array', value: ['redis-5-cache:test-key'] }, - }), - }), - ]); - - createEsmAndCjsTests(__dirname, 'scenario-redis-5.mjs', 'instrument-redis-5.mjs', (createTestRunner, test) => { - test('should create cache spans for prefixed keys (redis-5)', async () => { - await createTestRunner() - .expect({ transaction: EXPECTED_REDIS_CONNECT }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .start() - .completed(); - }); - - test('should name cache spans after the cache operation when streamed (redis-5)', async () => { - await createTestRunner() - .withEnv({ STREAMED: 'true' }) - // The connect span opens its own segment. `unordered` keeps this expectation from - // depending on which segment the buffer happens to emit first. - .unordered() - .expect({ span: { items: EXPECTED_STREAMED_SPANS } }) - .start() - .completed(); - }); }); }); describe('streamed', () => { - // The blocks above assert the same commands as transactions. With span streaming, span names - // have to be low cardinality, so `db.query` spans drop the serialized statement from their - // name — it stays on `db.query.text` — and are named - // `{db.operation.name} {server.address}:{server.port}` instead. Cache spans are named after - // their cache operation, and batch spans keep their `MULTI`/`PIPELINE` name. const streamAttribute = (value: unknown): { type: string; value: unknown } => ({ type: Array.isArray(value) ? 'array' : Number.isInteger(value) ? 'integer' : typeof value, value, @@ -701,18 +171,18 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory const commonAttributes = (segmentName: string): Record => ({ ...streamAttributes({ - 'db.system.name': 'redis', - 'sentry.environment': 'production', - 'sentry.kind': 'client', - 'sentry.origin': redisOrigin, - 'sentry.release': '1.0', - 'sentry.sdk.name': EXPECTED_SDK_NAME, - 'sentry.segment.name': segmentName, + [DB_SYSTEM_NAME]: 'redis', + [SENTRY_ENVIRONMENT]: 'production', + [SENTRY_KIND]: 'client', + [SENTRY_ORIGIN]: redisOrigin, + [SENTRY_RELEASE]: '1.0', + [SENTRY_SDK_NAME]: EXPECTED_SDK_NAME, + [SENTRY_SEGMENT_NAME]: segmentName, [SENTRY_TRACE_LIFECYCLE]: 'stream', }), - 'sentry.sdk.version': { type: 'string', value: expect.any(String) }, - 'sentry.is_localhost': { type: 'boolean', value: false }, - 'sentry.segment.id': { type: 'string', value: expect.stringMatching(/^[\da-f]{16}$/) }, + [SENTRY_SDK_VERSION]: { type: 'string', value: expect.any(String) }, + [SENTRY_IS_LOCALHOST]: { type: 'boolean', value: false }, + [SENTRY_SEGMENT_ID]: { type: 'string', value: expect.stringMatching(/^[\da-f]{16}$/) }, }); function streamedSpan({ @@ -732,7 +202,7 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory name, attributes: { ...commonAttributes(segmentName), - ...streamAttributes({ 'sentry.op': op, ...attributes }), + ...streamAttributes({ [SENTRY_OP]: op, ...attributes }), }, end_timestamp: expect.any(Number), is_segment: false, @@ -749,8 +219,8 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory describe('ioredis', () => { const segmentName = 'Test Span'; - const connection = { 'server.address': 'localhost', 'server.port': 6383 }; - const peer = { 'network.peer.address': 'localhost', 'network.peer.port': 6383 }; + const connection = { [SERVER_ADDRESS]: 'localhost', [SERVER_PORT]: 6383 }; + const peer = { [NETWORK_PEER_ADDRESS]: 'localhost', [NETWORK_PEER_PORT]: 6383 }; const span = (name: string, op: string, attributes: Record, status?: string): unknown => streamedSpan({ name, op, segmentName, status, attributes: { ...connection, ...attributes } }); @@ -758,69 +228,69 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory // A cache span is a db span the cache hook took over: it is renamed to its cache operation // and reports the connection it inherited as peer attributes too. const cacheSpan = ( - op: 'cache.get' | 'cache.put' | 'cache.remove', + op: typeof CACHE_GET | typeof CACHE_PUT | typeof CACHE_REMOVE, attributes: Record, - ): unknown => span(op, op, { ...peer, 'cache.operation': op.slice('cache.'.length), ...attributes }); + ): unknown => span(op, op, { ...peer, [CACHE_OPERATION]: op.slice('cache.'.length), ...attributes }); createEsmAndCjsTests(__dirname, 'scenario-ioredis.mjs', 'instrument-ioredis.mjs', (createTestRunner, test) => { - test('creates streamed db and cache spans (ioredis)', { timeout: 60_000 }, async () => { + test('should create cache spans for prefixed keys (ioredis)', { timeout: 60_000 }, async () => { await createTestRunner() .withEnv({ STREAMED: 'true' }) .expect({ - span: (container: SerializedStreamedSpanContainer) => { + span: container => { expect(container.items.find(item => item.is_segment)?.name).toBe(segmentName); expect(childSpans(container)).toEqual([ span('set localhost:6383', redisSpanOp, { - 'db.operation.name': 'set', - 'db.query.text': 'set test-key [1 other arguments]', + [DB_OPERATION_NAME]: 'set', + [DB_QUERY_TEXT]: 'set test-key [1 other arguments]', }), - cacheSpan('cache.put', { - 'db.operation.name': 'set', - 'db.query.text': 'set ioredis-cache:test-key [1 other arguments]', - 'cache.key': ['ioredis-cache:test-key'], - 'cache.item_size': 2, + cacheSpan(CACHE_PUT, { + [DB_OPERATION_NAME]: 'set', + [DB_QUERY_TEXT]: 'set ioredis-cache:test-key [1 other arguments]', + [CACHE_KEY]: ['ioredis-cache:test-key'], + [CACHE_ITEM_SIZE]: 2, }), - cacheSpan('cache.put', { - 'db.operation.name': 'set', - 'db.query.text': 'set ioredis-cache:test-key-set-EX [3 other arguments]', - 'cache.key': ['ioredis-cache:test-key-set-EX'], - 'cache.item_size': 2, + cacheSpan(CACHE_PUT, { + [DB_OPERATION_NAME]: 'set', + [DB_QUERY_TEXT]: 'set ioredis-cache:test-key-set-EX [3 other arguments]', + [CACHE_KEY]: ['ioredis-cache:test-key-set-EX'], + [CACHE_ITEM_SIZE]: 2, }), - cacheSpan('cache.put', { - 'db.operation.name': 'setex', - 'db.query.text': 'setex ioredis-cache:test-key-setex [2 other arguments]', - 'cache.key': ['ioredis-cache:test-key-setex'], - 'cache.item_size': 2, + cacheSpan(CACHE_PUT, { + [DB_OPERATION_NAME]: 'setex', + [DB_QUERY_TEXT]: 'setex ioredis-cache:test-key-setex [2 other arguments]', + [CACHE_KEY]: ['ioredis-cache:test-key-setex'], + [CACHE_ITEM_SIZE]: 2, }), span('get localhost:6383', redisSpanOp, { - 'db.operation.name': 'get', - 'db.query.text': 'get test-key', + [DB_OPERATION_NAME]: 'get', + [DB_QUERY_TEXT]: 'get test-key', }), - cacheSpan('cache.get', { - 'db.operation.name': 'get', - 'db.query.text': 'get ioredis-cache:test-key', - 'cache.key': ['ioredis-cache:test-key'], - 'cache.hit': true, - 'cache.item_size': 10, + cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'get', + [DB_QUERY_TEXT]: 'get ioredis-cache:test-key', + [CACHE_KEY]: ['ioredis-cache:test-key'], + [CACHE_HIT]: true, + [CACHE_ITEM_SIZE]: 10, }), - cacheSpan('cache.get', { - 'db.operation.name': 'get', - 'db.query.text': 'get ioredis-cache:unavailable-data', - 'cache.key': ['ioredis-cache:unavailable-data'], - 'cache.hit': false, + cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'get', + [DB_QUERY_TEXT]: 'get ioredis-cache:unavailable-data', + [CACHE_KEY]: ['ioredis-cache:unavailable-data'], + [CACHE_HIT]: false, }), - cacheSpan('cache.get', { - 'db.operation.name': 'mget', - 'db.query.text': 'mget [3 other arguments]', - 'cache.key': ['test-key', 'ioredis-cache:test-key', 'ioredis-cache:unavailable-data'], - 'cache.hit': true, - 'cache.item_size': 20, + cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'mget', + [DB_QUERY_TEXT]: 'mget [3 other arguments]', + [CACHE_KEY]: ['test-key', 'ioredis-cache:test-key', 'ioredis-cache:unavailable-data'], + [CACHE_HIT]: true, + [CACHE_ITEM_SIZE]: 20, }), - cacheSpan('cache.remove', { - 'db.operation.name': 'del', - 'db.query.text': 'del ioredis-cache:test-key', - 'cache.key': ['ioredis-cache:test-key'], + cacheSpan(CACHE_REMOVE, { + [DB_OPERATION_NAME]: 'del', + [DB_QUERY_TEXT]: 'del ioredis-cache:test-key', + [CACHE_KEY]: ['ioredis-cache:test-key'], }), ]); }, @@ -835,8 +305,8 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory // `{db.operation.name} {server.address}:{server.port}` name. describe('redis-4', () => { const segmentName = 'Test Span Redis 4'; - const connection = { 'server.address': 'localhost', 'server.port': 6383 }; - const peer = { 'network.peer.address': 'localhost', 'network.peer.port': 6383 }; + const connection = { [SERVER_ADDRESS]: 'localhost', [SERVER_PORT]: 6383 }; + const peer = { [NETWORK_PEER_ADDRESS]: 'localhost', [NETWORK_PEER_PORT]: 6383 }; const span = (name: string, op: string, attributes: Record, status?: string): unknown => streamedSpan({ name, op, segmentName, status, attributes: { ...connection, ...attributes } }); @@ -844,16 +314,15 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory // A cache span is a db span the cache hook took over: it is renamed to its cache operation // and reports the connection it inherited as peer attributes too. const cacheSpan = ( - op: 'cache.get' | 'cache.put' | 'cache.remove', + op: typeof CACHE_GET | typeof CACHE_PUT | typeof CACHE_REMOVE, attributes: Record, - ): unknown => span(op, op, { ...peer, 'cache.operation': op.slice('cache.'.length), ...attributes }); + ): unknown => span(op, op, { ...peer, [CACHE_OPERATION]: op.slice('cache.'.length), ...attributes }); createEsmAndCjsTests(__dirname, 'scenario-redis-4.mjs', 'instrument-redis-4.mjs', (createTestRunner, test) => { - test('creates streamed db and cache spans (redis-4)', { timeout: 60_000 }, async () => { + test('should create cache spans for prefixed keys (redis-4)', { timeout: 60_000 }, async () => { await createTestRunner() - .withEnv({ STREAMED: 'true' }) .expect({ - span: (container: SerializedStreamedSpanContainer) => { + span: container => { // The connect span opens its own segment, but shares the trace with the test span, // so both segments arrive in the same container. expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ @@ -863,66 +332,66 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory expect(childSpans(container)).toEqual([ span('SET localhost:6383', redisSpanOp, { - 'db.operation.name': 'SET', - 'db.query.text': 'SET redis-test-key [1 other arguments]', + [DB_OPERATION_NAME]: 'SET', + [DB_QUERY_TEXT]: 'SET redis-test-key [1 other arguments]', }), - cacheSpan('cache.put', { - 'db.operation.name': 'SET', - 'db.query.text': 'SET redis-cache:test-key [1 other arguments]', - 'cache.key': ['redis-cache:test-key'], - 'cache.item_size': 2, + cacheSpan(CACHE_PUT, { + [DB_OPERATION_NAME]: 'SET', + [DB_QUERY_TEXT]: 'SET redis-cache:test-key [1 other arguments]', + [CACHE_KEY]: ['redis-cache:test-key'], + [CACHE_ITEM_SIZE]: 2, }), - cacheSpan('cache.put', { - 'db.operation.name': 'SET', - 'db.query.text': 'SET redis-cache:test-key-set-EX [3 other arguments]', - 'cache.key': ['redis-cache:test-key-set-EX'], - 'cache.item_size': 2, + cacheSpan(CACHE_PUT, { + [DB_OPERATION_NAME]: 'SET', + [DB_QUERY_TEXT]: 'SET redis-cache:test-key-set-EX [3 other arguments]', + [CACHE_KEY]: ['redis-cache:test-key-set-EX'], + [CACHE_ITEM_SIZE]: 2, }), - cacheSpan('cache.put', { - 'db.operation.name': 'SETEX', - 'db.query.text': 'SETEX redis-cache:test-key-setex [2 other arguments]', - 'cache.key': ['redis-cache:test-key-setex'], - 'cache.item_size': 2, + cacheSpan(CACHE_PUT, { + [DB_OPERATION_NAME]: 'SETEX', + [DB_QUERY_TEXT]: 'SETEX redis-cache:test-key-setex [2 other arguments]', + [CACHE_KEY]: ['redis-cache:test-key-setex'], + [CACHE_ITEM_SIZE]: 2, }), span('GET localhost:6383', redisSpanOp, { - 'db.operation.name': 'GET', - 'db.query.text': 'GET redis-test-key', + [DB_OPERATION_NAME]: 'GET', + [DB_QUERY_TEXT]: 'GET redis-test-key', }), - cacheSpan('cache.get', { - 'db.operation.name': 'GET', - 'db.query.text': 'GET redis-cache:test-key', - 'cache.key': ['redis-cache:test-key'], - 'cache.hit': true, - 'cache.item_size': 10, + cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'GET', + [DB_QUERY_TEXT]: 'GET redis-cache:test-key', + [CACHE_KEY]: ['redis-cache:test-key'], + [CACHE_HIT]: true, + [CACHE_ITEM_SIZE]: 10, }), - cacheSpan('cache.get', { - 'db.operation.name': 'GET', - 'db.query.text': 'GET redis-cache:unavailable-data', - 'cache.key': ['redis-cache:unavailable-data'], - 'cache.hit': false, + cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'GET', + [DB_QUERY_TEXT]: 'GET redis-cache:unavailable-data', + [CACHE_KEY]: ['redis-cache:unavailable-data'], + [CACHE_HIT]: false, }), - cacheSpan('cache.get', { - 'db.operation.name': 'MGET', - 'db.query.text': 'MGET [3 other arguments]', - 'cache.key': ['redis-test-key', 'redis-cache:test-key', 'redis-cache:unavailable-data'], - 'cache.hit': true, - 'cache.item_size': 20, + cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'MGET', + [DB_QUERY_TEXT]: 'MGET [3 other arguments]', + [CACHE_KEY]: ['redis-test-key', 'redis-cache:test-key', 'redis-cache:unavailable-data'], + [CACHE_HIT]: true, + [CACHE_ITEM_SIZE]: 20, }), - cacheSpan('cache.remove', { - 'db.operation.name': 'DEL', - 'db.query.text': 'DEL redis-cache:test-key', - 'cache.key': ['redis-cache:test-key'], + cacheSpan(CACHE_REMOVE, { + [DB_OPERATION_NAME]: 'DEL', + [DB_QUERY_TEXT]: 'DEL redis-cache:test-key', + [CACHE_KEY]: ['redis-cache:test-key'], }), // Batch spans are named after the batch operation, which is already low cardinality. - span('MULTI', redisSpanOp, { 'db.operation.name': 'MULTI', 'db.operation.batch.size': 2 }), + span('MULTI', redisSpanOp, { [DB_OPERATION_NAME]: 'MULTI', [DB_OPERATION_BATCH_SIZE]: 2 }), span( 'INCR localhost:6383', redisSpanOp, { - 'db.operation.name': 'INCR', - 'db.query.text': 'INCR redis-test-key', - 'error.type': 'Error', - 'sentry.status.message': 'ERR value is not an integer or out of range', + [DB_OPERATION_NAME]: 'INCR', + [DB_QUERY_TEXT]: 'INCR redis-test-key', + [ERROR_TYPE]: 'Error', + [SENTRY_STATUS_MESSAGE]: 'ERR value is not an integer or out of range', }, 'error', ), @@ -940,8 +409,8 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory // connection and get the same span name. describe('redis-5', () => { const segmentName = 'Test Span Redis 5'; - const connection = { 'server.address': 'localhost', 'server.port': 6383 }; - const peer = { 'network.peer.address': 'localhost', 'network.peer.port': 6383 }; + const connection = { [SERVER_ADDRESS]: 'localhost', [SERVER_PORT]: 6383 }; + const peer = { [NETWORK_PEER_ADDRESS]: 'localhost', [NETWORK_PEER_PORT]: 6383 }; const span = (name: string, op: string, attributes: Record, status?: string): unknown => streamedSpan({ name, op, segmentName, status, attributes: { ...connection, ...attributes } }); @@ -949,16 +418,15 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory // A cache span is a db span the cache hook took over: it is renamed to its cache operation // and reports the connection it inherited as peer attributes too. const cacheSpan = ( - op: 'cache.get' | 'cache.put' | 'cache.remove', + op: typeof CACHE_GET | typeof CACHE_PUT | typeof CACHE_REMOVE, attributes: Record, - ): unknown => span(op, op, { ...peer, 'cache.operation': op.slice('cache.'.length), ...attributes }); + ): unknown => span(op, op, { ...peer, [CACHE_OPERATION]: op.slice('cache.'.length), ...attributes }); createEsmAndCjsTests(__dirname, 'scenario-redis-5.mjs', 'instrument-redis-5.mjs', (createTestRunner, test) => { - test('creates streamed db and cache spans (redis-5)', { timeout: 60_000 }, async () => { + test('should create cache spans for prefixed keys (redis-5)', { timeout: 60_000 }, async () => { await createTestRunner() - .withEnv({ STREAMED: 'true' }) .expect({ - span: (container: SerializedStreamedSpanContainer) => { + span: container => { expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ 'redis-connect', segmentName, @@ -966,65 +434,65 @@ describeWithDockerCompose('redis cache auto instrumentation', { workingDirectory expect(childSpans(container)).toEqual([ span('SET localhost:6383', redisSpanOp, { - 'db.operation.name': 'SET', - 'db.query.text': 'SET redis-5-test-key [1 other arguments]', + [DB_OPERATION_NAME]: 'SET', + [DB_QUERY_TEXT]: 'SET redis-5-test-key [1 other arguments]', }), - cacheSpan('cache.put', { - 'db.operation.name': 'SET', - 'db.query.text': 'SET redis-5-cache:test-key [1 other arguments]', - 'cache.key': ['redis-5-cache:test-key'], - 'cache.item_size': 2, + cacheSpan(CACHE_PUT, { + [DB_OPERATION_NAME]: 'SET', + [DB_QUERY_TEXT]: 'SET redis-5-cache:test-key [1 other arguments]', + [CACHE_KEY]: ['redis-5-cache:test-key'], + [CACHE_ITEM_SIZE]: 2, }), - cacheSpan('cache.put', { - 'db.operation.name': 'SET', - 'db.query.text': 'SET redis-5-cache:test-key-set-EX [3 other arguments]', - 'cache.key': ['redis-5-cache:test-key-set-EX'], - 'cache.item_size': 2, + cacheSpan(CACHE_PUT, { + [DB_OPERATION_NAME]: 'SET', + [DB_QUERY_TEXT]: 'SET redis-5-cache:test-key-set-EX [3 other arguments]', + [CACHE_KEY]: ['redis-5-cache:test-key-set-EX'], + [CACHE_ITEM_SIZE]: 2, }), - cacheSpan('cache.put', { - 'db.operation.name': 'SETEX', - 'db.query.text': 'SETEX redis-5-cache:test-key-setex [2 other arguments]', - 'cache.key': ['redis-5-cache:test-key-setex'], - 'cache.item_size': 2, + cacheSpan(CACHE_PUT, { + [DB_OPERATION_NAME]: 'SETEX', + [DB_QUERY_TEXT]: 'SETEX redis-5-cache:test-key-setex [2 other arguments]', + [CACHE_KEY]: ['redis-5-cache:test-key-setex'], + [CACHE_ITEM_SIZE]: 2, }), span('GET localhost:6383', redisSpanOp, { - 'db.operation.name': 'GET', - 'db.query.text': 'GET redis-5-test-key', + [DB_OPERATION_NAME]: 'GET', + [DB_QUERY_TEXT]: 'GET redis-5-test-key', }), - cacheSpan('cache.get', { - 'db.operation.name': 'GET', - 'db.query.text': 'GET redis-5-cache:test-key', - 'cache.key': ['redis-5-cache:test-key'], - 'cache.hit': true, - 'cache.item_size': 10, + cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'GET', + [DB_QUERY_TEXT]: 'GET redis-5-cache:test-key', + [CACHE_KEY]: ['redis-5-cache:test-key'], + [CACHE_HIT]: true, + [CACHE_ITEM_SIZE]: 10, }), - cacheSpan('cache.get', { - 'db.operation.name': 'GET', - 'db.query.text': 'GET redis-5-cache:unavailable-data', - 'cache.key': ['redis-5-cache:unavailable-data'], - 'cache.hit': false, + cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'GET', + [DB_QUERY_TEXT]: 'GET redis-5-cache:unavailable-data', + [CACHE_KEY]: ['redis-5-cache:unavailable-data'], + [CACHE_HIT]: false, }), - cacheSpan('cache.get', { - 'db.operation.name': 'MGET', - 'db.query.text': 'MGET [3 other arguments]', - 'cache.key': ['redis-5-test-key', 'redis-5-cache:test-key', 'redis-5-cache:unavailable-data'], - 'cache.hit': true, - 'cache.item_size': 20, + cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'MGET', + [DB_QUERY_TEXT]: 'MGET [3 other arguments]', + [CACHE_KEY]: ['redis-5-test-key', 'redis-5-cache:test-key', 'redis-5-cache:unavailable-data'], + [CACHE_HIT]: true, + [CACHE_ITEM_SIZE]: 20, }), - cacheSpan('cache.remove', { - 'db.operation.name': 'DEL', - 'db.query.text': 'DEL redis-5-cache:test-key', - 'cache.key': ['redis-5-cache:test-key'], + cacheSpan(CACHE_REMOVE, { + [DB_OPERATION_NAME]: 'DEL', + [DB_QUERY_TEXT]: 'DEL redis-5-cache:test-key', + [CACHE_KEY]: ['redis-5-cache:test-key'], }), - span('MULTI', redisSpanOp, { 'db.operation.name': 'MULTI', 'db.operation.batch.size': 2 }), + span('MULTI', redisSpanOp, { [DB_OPERATION_NAME]: 'MULTI', [DB_OPERATION_BATCH_SIZE]: 2 }), span( 'INCR localhost:6383', redisSpanOp, { - 'db.operation.name': 'INCR', - 'db.query.text': 'INCR redis-5-test-key', - 'error.type': 'Error', - 'sentry.status.message': 'ERR value is not an integer or out of range', + [DB_OPERATION_NAME]: 'INCR', + [DB_QUERY_TEXT]: 'INCR redis-5-test-key', + [ERROR_TYPE]: 'Error', + [SENTRY_STATUS_MESSAGE]: 'ERR value is not an integer or out of range', }, 'error', ), diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-dc/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/redis-dc/instrument.mjs index d4f8cc7801fc..28e109af9952 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-dc/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/redis-dc/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts index 06d8cac5a465..a5bf7450724d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts @@ -1,6 +1,30 @@ -import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; -import type { SerializedStreamedSpanContainer } from '@sentry/core'; -import { afterAll, describe, expect } from 'vitest'; +import { + CACHE_HIT, + CACHE_ITEM_SIZE, + CACHE_KEY, + CACHE_OPERATION, + DB_OPERATION_NAME, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + ERROR_TYPE, + NETWORK_PEER_ADDRESS, + NETWORK_PEER_PORT, + SENTRY_ENVIRONMENT, + SENTRY_IS_LOCALHOST, + SENTRY_OP, + SENTRY_ORIGIN, + SENTRY_RELEASE, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_SEGMENT_ID, + SENTRY_SEGMENT_NAME, + SENTRY_STATUS_MESSAGE, + SENTRY_TRACE_LIFECYCLE, + SERVER_ADDRESS, + SERVER_PORT, +} from '@sentry/conventions/attributes'; +import { CACHE_GET, CACHE_PUT, type CACHE_REMOVE, DB_QUERY } from '@sentry/conventions/op'; +import { afterAll, expect } from 'vitest'; import { EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -12,134 +36,21 @@ describeWithDockerCompose( cleanupChildProcesses(); }); - const EXPECTED_TRANSACTION = { - transaction: 'Test Span Redis 5 DC', - spans: expect.arrayContaining([ - expect.objectContaining({ - op: 'db.query', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.op': 'db.query', - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.system.name': 'redis', - 'db.query.text': 'SET dc-test-key ?', - }), - }), - // cache SET: starts as a cache span, named by its key in the static lifecycle - expect.objectContaining({ - description: 'dc-cache:test-key', - op: 'cache.put', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.query.text': 'SET dc-cache:test-key ?', - 'cache.key': ['dc-cache:test-key'], - 'cache.item_size': 2, - }), - }), - // cache SET with EX option: redis v5 sends SET key value EX 10 as the command - expect.objectContaining({ - description: 'dc-cache:test-key-ex', - op: 'cache.put', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.query.text': 'SET dc-cache:test-key-ex ? ? ?', - 'cache.key': ['dc-cache:test-key-ex'], - 'cache.item_size': 2, - }), - }), - expect.objectContaining({ - op: 'db.query', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.op': 'db.query', - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.system.name': 'redis', - 'db.query.text': 'GET dc-test-key', - }), - }), - // cache GET (hit) - expect.objectContaining({ - description: 'dc-cache:test-key', - op: 'cache.get', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.query.text': 'GET dc-cache:test-key', - 'cache.hit': true, - 'cache.key': ['dc-cache:test-key'], - 'cache.item_size': 10, - }), - }), - // cache GET (miss) - expect.objectContaining({ - description: 'dc-cache:unavailable-data', - op: 'cache.get', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.query.text': 'GET dc-cache:unavailable-data', - 'cache.hit': false, - 'cache.key': ['dc-cache:unavailable-data'], - }), - }), - // MGET: node-redis sanitizes args for diagnostics_channel (keys become '?'), - // so cache detection cannot match prefixes — remains a plain db.query span. - expect.objectContaining({ - op: 'db.query', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'sentry.op': 'db.query', - 'sentry.origin': 'auto.db.redis.diagnostic_channel', - 'db.system.name': 'redis', - 'db.query.text': 'MGET ? ? ?', - }), - }), - // a failing command on a cache key reports as an errored cache span: - // the span starts as a cache span, so the classification survives the error - expect.objectContaining({ - description: 'dc-cache:list-key', - op: 'cache.get', - status: 'internal_error', - origin: 'auto.db.redis.diagnostic_channel', - data: expect.objectContaining({ - 'cache.operation': 'get', - 'cache.key': ['dc-cache:list-key'], - }), - }), - ]), - }; - - // node-redis emits a node-redis:connect DC event for the initial connection. - // That fires before startSpan so it arrives as the first envelope. - const EXPECTED_CONNECT = { - transaction: 'redis-connect', - }; - createEsmAndCjsTests(__dirname, 'scenario-redis-5-tracing.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('should create spans for redis v5 commands via diagnostics_channel', { timeout: 60_000 }, async () => { - await createTestRunner() - .expect({ transaction: EXPECTED_CONNECT }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .start() - .completed(); - }); - // `ignoreSpans` is evaluated at span start under streaming, so this only passes because the // span starts as a cache span — a db span renamed at response time would slip through. test('drops cache spans matching an ignoreSpans op filter at span start', { timeout: 60_000 }, async () => { await createTestRunner() - .withEnv({ STREAMED: 'true', IGNORE_CACHE_GET: 'true' }) + .withEnv({ IGNORE_CACHE_GET: 'true' }) .unignore('client_report') // The span container and the client report flush on independent timers, so they can // arrive in either order. .unordered() .expect({ - span: (container: SerializedStreamedSpanContainer) => { + span: container => { const names = container.items.map(item => item.name); - expect(names).toContain('cache.put'); - expect(names).not.toContain('cache.get'); + expect(names).toContain(CACHE_PUT); + expect(names).not.toContain(CACHE_GET); }, }) .expect({ @@ -155,148 +66,142 @@ describeWithDockerCompose( }); }); - // The same commands as above, asserted on the streamed span container. With span streaming the - // db spans are named `{db.operation.name} {server.address}:{server.port}` instead of - // `redis-{command}`. - describe('streamed', () => { - const ORIGIN = 'auto.db.redis.diagnostic_channel'; - const SEGMENT_NAME = 'Test Span Redis 5 DC'; - const HOST = '127.0.0.1'; - const PORT = 6381; + const ORIGIN = 'auto.db.redis.diagnostic_channel'; + const SEGMENT_NAME = 'Test Span Redis 5 DC'; + const HOST = '127.0.0.1'; + const PORT = 6381; - const streamAttribute = (value: unknown): { type: string; value: unknown } => ({ - type: Array.isArray(value) ? 'array' : Number.isInteger(value) ? 'integer' : typeof value, - value, - }); + const streamAttribute = (value: unknown): { type: string; value: unknown } => ({ + type: Array.isArray(value) ? 'array' : Number.isInteger(value) ? 'integer' : typeof value, + value, + }); - // Streamed spans carry `{ type, value }` attribute pairs; the expectations below are written - // as plain values and wrapped here. - const streamAttributes = (values: Record): Record => - Object.fromEntries(Object.entries(values).map(([key, value]) => [key, streamAttribute(value)])); + // Streamed spans carry `{ type, value }` attribute pairs; the expectations below are written + // as plain values and wrapped here. + const streamAttributes = (values: Record): Record => + Object.fromEntries(Object.entries(values).map(([key, value]) => [key, streamAttribute(value)])); - function streamedSpan(name: string, op: string, attributes: Record): unknown { - return { - name, - attributes: { - ...streamAttributes({ - 'db.system.name': 'redis', - 'sentry.environment': 'production', - 'sentry.op': op, - 'sentry.origin': ORIGIN, - 'sentry.release': '1.0', - 'sentry.sdk.name': EXPECTED_SDK_NAME, - 'sentry.segment.name': SEGMENT_NAME, - 'server.address': HOST, - 'server.port': PORT, - [SENTRY_TRACE_LIFECYCLE]: 'stream', - ...attributes, - }), - 'sentry.sdk.version': { type: 'string', value: expect.any(String) }, - 'sentry.segment.id': { type: 'string', value: expect.stringMatching(/^[\da-f]{16}$/) }, - 'sentry.is_localhost': { type: 'boolean', value: false }, - }, - end_timestamp: expect.any(Number), - is_segment: false, - parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - start_timestamp: expect.any(Number), - status: 'ok', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - }; - } + function streamedSpan(name: string, op: string, attributes: Record): unknown { + return { + name, + attributes: { + ...streamAttributes({ + [DB_SYSTEM_NAME]: 'redis', + [SENTRY_ENVIRONMENT]: 'production', + [SENTRY_OP]: op, + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_RELEASE]: '1.0', + [SENTRY_SDK_NAME]: EXPECTED_SDK_NAME, + [SENTRY_SEGMENT_NAME]: SEGMENT_NAME, + [SERVER_ADDRESS]: HOST, + [SERVER_PORT]: PORT, + [SENTRY_TRACE_LIFECYCLE]: 'stream', + ...attributes, + }), + [SENTRY_SDK_VERSION]: { type: 'string', value: expect.any(String) }, + [SENTRY_SEGMENT_ID]: { type: 'string', value: expect.stringMatching(/^[\da-f]{16}$/) }, + [SENTRY_IS_LOCALHOST]: { type: 'boolean', value: false }, + }, + end_timestamp: expect.any(Number), + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + status: 'ok', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + }; + } - const PEER = { 'network.peer.address': HOST, 'network.peer.port': PORT }; + const PEER = { [NETWORK_PEER_ADDRESS]: HOST, [NETWORK_PEER_PORT]: PORT }; - // A cache span is a db span whose key matched a cache prefix: it starts named after its - // cache operation and reports the connection as peer attributes too. - const cacheSpan = ( - op: 'cache.get' | 'cache.put' | 'cache.remove', - attributes: Record, - ): unknown => streamedSpan(op, op, { ...PEER, 'cache.operation': op.slice('cache.'.length), ...attributes }); + // A cache span is a db span whose key matched a cache prefix: it starts named after its + // cache operation and reports the connection as peer attributes too. + const cacheSpan = ( + op: typeof CACHE_GET | typeof CACHE_PUT | typeof CACHE_REMOVE, + attributes: Record, + ): unknown => streamedSpan(op, op, { ...PEER, [CACHE_OPERATION]: op.slice('cache.'.length), ...attributes }); - createEsmAndCjsTests(__dirname, 'scenario-redis-5-tracing.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('creates streamed spans for redis v5 commands via diagnostics_channel', { timeout: 60_000 }, async () => { - await createTestRunner() - .withEnv({ STREAMED: 'true' }) - .expect({ - span: (container: SerializedStreamedSpanContainer) => { - // The connect span opens its own segment but shares the trace with the test span, - // so both segments arrive in the same container. - expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ - 'redis-connect', - SEGMENT_NAME, - ]); + createEsmAndCjsTests(__dirname, 'scenario-redis-5-tracing.mjs', 'instrument.mjs', (createTestRunner, test) => { + test('should create spans for redis v5 commands via diagnostics_channel', { timeout: 60_000 }, async () => { + await createTestRunner() + .expect({ + span: container => { + // The connect span opens its own segment but shares the trace with the test span, + // so both segments arrive in the same container. + expect(container.items.filter(item => item.is_segment).map(item => item.name)).toEqual([ + 'redis-connect', + SEGMENT_NAME, + ]); - const spans = container.items.filter( - item => !item.is_segment && item.attributes['sentry.segment.name']?.value === SEGMENT_NAME, - ); + const spans = container.items.filter( + item => !item.is_segment && item.attributes[SENTRY_SEGMENT_NAME]?.value === SEGMENT_NAME, + ); - expect(spans).toEqual([ - streamedSpan(`SET ${HOST}:${PORT}`, 'db.query', { - 'db.operation.name': 'SET', - 'db.query.text': 'SET dc-test-key ?', - }), - // cache SET: starts as a cache span - cacheSpan('cache.put', { - 'db.operation.name': 'SET', - 'db.query.text': 'SET dc-cache:test-key ?', - 'cache.key': ['dc-cache:test-key'], - 'cache.item_size': 2, - }), - // cache SET with EX option: redis v5 sends SET key value EX 10 as the command - cacheSpan('cache.put', { - 'db.operation.name': 'SET', - 'db.query.text': 'SET dc-cache:test-key-ex ? ? ?', - 'cache.key': ['dc-cache:test-key-ex'], - 'cache.item_size': 2, - }), - streamedSpan(`GET ${HOST}:${PORT}`, 'db.query', { - 'db.operation.name': 'GET', - 'db.query.text': 'GET dc-test-key', - }), - // cache GET (hit) - cacheSpan('cache.get', { - 'db.operation.name': 'GET', - 'db.query.text': 'GET dc-cache:test-key', - 'cache.key': ['dc-cache:test-key'], - 'cache.hit': true, - 'cache.item_size': 10, - }), - // cache GET (miss) - cacheSpan('cache.get', { - 'db.operation.name': 'GET', - 'db.query.text': 'GET dc-cache:unavailable-data', - 'cache.key': ['dc-cache:unavailable-data'], - 'cache.hit': false, - }), - // MGET: node-redis sanitizes args for diagnostics_channel (keys become '?'), - // so cache detection cannot match prefixes — remains a plain db.query span. - streamedSpan(`MGET ${HOST}:${PORT}`, 'db.query', { - 'db.operation.name': 'MGET', - 'db.query.text': 'MGET ? ? ?', - }), - streamedSpan(`LPUSH ${HOST}:${PORT}`, 'db.query', { - 'db.operation.name': 'LPUSH', - 'db.query.text': 'LPUSH dc-cache:list-key ?', - }), - // a failing command on a cache key reports as an errored cache span: - // the span starts as a cache span, so the classification survives the error - { - ...(cacheSpan('cache.get', { - 'db.operation.name': 'GET', - 'db.query.text': 'GET dc-cache:list-key', - 'cache.key': ['dc-cache:list-key'], - 'error.type': 'Error', - 'sentry.status.message': 'WRONGTYPE Operation against a key holding the wrong kind of value', - }) as Record), - status: 'error', - }, - ]); - }, - }) - .start() - .completed(); - }); + expect(spans).toEqual([ + streamedSpan(`SET ${HOST}:${PORT}`, DB_QUERY, { + [DB_OPERATION_NAME]: 'SET', + [DB_QUERY_TEXT]: 'SET dc-test-key ?', + }), + // cache SET: starts as a cache span + cacheSpan(CACHE_PUT, { + [DB_OPERATION_NAME]: 'SET', + [DB_QUERY_TEXT]: 'SET dc-cache:test-key ?', + [CACHE_KEY]: ['dc-cache:test-key'], + [CACHE_ITEM_SIZE]: 2, + }), + // cache SET with EX option: redis v5 sends SET key value EX 10 as the command + cacheSpan(CACHE_PUT, { + [DB_OPERATION_NAME]: 'SET', + [DB_QUERY_TEXT]: 'SET dc-cache:test-key-ex ? ? ?', + [CACHE_KEY]: ['dc-cache:test-key-ex'], + [CACHE_ITEM_SIZE]: 2, + }), + streamedSpan(`GET ${HOST}:${PORT}`, DB_QUERY, { + [DB_OPERATION_NAME]: 'GET', + [DB_QUERY_TEXT]: 'GET dc-test-key', + }), + // cache GET (hit) + cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'GET', + [DB_QUERY_TEXT]: 'GET dc-cache:test-key', + [CACHE_KEY]: ['dc-cache:test-key'], + [CACHE_HIT]: true, + [CACHE_ITEM_SIZE]: 10, + }), + // cache GET (miss) + cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'GET', + [DB_QUERY_TEXT]: 'GET dc-cache:unavailable-data', + [CACHE_KEY]: ['dc-cache:unavailable-data'], + [CACHE_HIT]: false, + }), + // MGET: node-redis sanitizes args for diagnostics_channel (keys become '?'), + // so cache detection cannot match prefixes — remains a plain db.query span. + streamedSpan(`MGET ${HOST}:${PORT}`, DB_QUERY, { + [DB_OPERATION_NAME]: 'MGET', + [DB_QUERY_TEXT]: 'MGET ? ? ?', + }), + streamedSpan(`LPUSH ${HOST}:${PORT}`, DB_QUERY, { + [DB_OPERATION_NAME]: 'LPUSH', + [DB_QUERY_TEXT]: 'LPUSH dc-cache:list-key ?', + }), + // a failing command on a cache key reports as an errored cache span: + // the span starts as a cache span, so the classification survives the error + { + ...(cacheSpan(CACHE_GET, { + [DB_OPERATION_NAME]: 'GET', + [DB_QUERY_TEXT]: 'GET dc-cache:list-key', + [CACHE_KEY]: ['dc-cache:list-key'], + [ERROR_TYPE]: 'Error', + [SENTRY_STATUS_MESSAGE]: 'WRONGTYPE Operation against a key holding the wrong kind of value', + }) as Record), + status: 'error', + }, + ]); + }, + }) + .start() + .completed(); }); }); }, diff --git a/dev-packages/node-integration-tests/suites/tracing/redis/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/redis/instrument.mjs index 22bf57f14364..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/redis/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, 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 d8b8b728a2af..36decf4b6001 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts @@ -1,6 +1,25 @@ -import { SENTRY_TRACE_LIFECYCLE, SENTRY_OP } from '@sentry/conventions/attributes'; -import { type SerializedStreamedSpanContainer } from '@sentry/core'; -import { afterAll, describe, expect } from 'vitest'; +import { + DB_OPERATION_NAME, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + ERROR_TYPE, + SENTRY_ENVIRONMENT, + SENTRY_IS_LOCALHOST, + SENTRY_KIND, + SENTRY_OP, + SENTRY_ORIGIN, + SENTRY_RELEASE, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_SEGMENT_ID, + SENTRY_SEGMENT_NAME, + SENTRY_STATUS_MESSAGE, + SENTRY_TRACE_LIFECYCLE, + SERVER_ADDRESS, + SERVER_PORT, +} from '@sentry/conventions/attributes'; +import { DB_QUERY } from '@sentry/conventions/op'; +import { afterAll, expect } from 'vitest'; import { EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -13,126 +32,66 @@ describeWithDockerCompose('redis auto instrumentation', { workingDirectory: [__d // subscriber instead of the OTel monkey-patch, so the span origin differs. All // other attributes are identical. const origin = 'auto.db.redis'; - const redisSpanOp = 'db.query'; - const redisData = { - 'db.system.name': 'redis', - 'server.address': 'localhost', - 'server.port': 6380, + const redisSpanOp = DB_QUERY; + const COMMON_ATTRIBUTES = { + [SENTRY_IS_LOCALHOST]: { type: 'boolean', value: false }, + [DB_SYSTEM_NAME]: { type: 'string', value: 'redis' }, + [SERVER_ADDRESS]: { type: 'string', value: 'localhost' }, + [SERVER_PORT]: { type: 'integer', value: 6380 }, + [SENTRY_KIND]: { type: 'string', value: 'client' }, + [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, + [SENTRY_OP]: { type: 'string', value: redisSpanOp }, + [SENTRY_ORIGIN]: { type: 'string', value: origin }, + [SENTRY_RELEASE]: { type: 'string', value: '1.0' }, + [SENTRY_SDK_NAME]: { type: 'string', value: EXPECTED_SDK_NAME }, + [SENTRY_SDK_VERSION]: { type: 'string', value: expect.any(String) }, + [SENTRY_SEGMENT_ID]: { type: 'string', value: expect.stringMatching(/^[\da-f]{16}$/) }, + [SENTRY_SEGMENT_NAME]: { type: 'string', value: 'Test Span' }, + [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, }; - const EXPECTED_TRANSACTION = { - transaction: 'Test Span', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'set test-key [1 other arguments]', - op: redisSpanOp, - origin, - data: expect.objectContaining({ - 'sentry.op': redisSpanOp, - 'sentry.origin': origin, - ...redisData, - 'db.query.text': 'set test-key [1 other arguments]', - }), - }), - expect.objectContaining({ - description: 'get test-key', - op: redisSpanOp, - origin, - data: expect.objectContaining({ - 'sentry.op': redisSpanOp, - 'sentry.origin': origin, - ...redisData, - 'db.query.text': 'get test-key', - }), - }), - // a failing command produces a span with an error status - expect.objectContaining({ - description: 'incr test-key', - op: redisSpanOp, - status: 'internal_error', - origin, - data: expect.objectContaining({ - 'sentry.op': redisSpanOp, - 'sentry.origin': origin, - ...redisData, - 'db.query.text': 'incr test-key', - }), - }), - ]), - }; + function expectedDbSpan({ + operation, + statement, + status = 'ok', + errorMessage, + }: { + operation: string; + statement: string; + status?: string; + errorMessage?: string; + }): unknown { + return { + attributes: { + ...COMMON_ATTRIBUTES, + [DB_OPERATION_NAME]: { type: 'string', value: operation }, + [DB_QUERY_TEXT]: { type: 'string', value: statement }, + ...(errorMessage + ? { + [ERROR_TYPE]: { type: 'string', value: 'ReplyError' }, + [SENTRY_STATUS_MESSAGE]: { type: 'string', value: errorMessage }, + } + : {}), + }, + name: `${operation} localhost:6380`, + end_timestamp: expect.any(Number), + is_segment: false, + parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + status, + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + }; + } createEsmAndCjsTests(__dirname, 'scenario-ioredis.mjs', 'instrument.mjs', (createTestRunner, test) => { test( 'should auto-instrument `ioredis` package when using redis.set() and redis.get()', { timeout: 75_000 }, async () => { - await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); - }, - ); - }); - - describe('streamed', () => { - // The same three commands as above, asserted on the streamed span container. Only the span - // name differs: with span streaming names have to be low cardinality, so the serialized - // statement is reported through `db.query.text` alone and the name becomes - // `{db.operation.name} {server.address}:{server.port}`. - const COMMON_ATTRIBUTES = { - 'sentry.is_localhost': { type: 'boolean', value: false }, - 'db.system.name': { type: 'string', value: 'redis' }, - 'server.address': { type: 'string', value: 'localhost' }, - 'server.port': { type: 'integer', value: 6380 }, - 'sentry.kind': { type: 'string', value: 'client' }, - 'sentry.environment': { type: 'string', value: 'production' }, - 'sentry.op': { type: 'string', value: redisSpanOp }, - 'sentry.origin': { type: 'string', value: origin }, - 'sentry.release': { type: 'string', value: '1.0' }, - 'sentry.sdk.name': { type: 'string', value: EXPECTED_SDK_NAME }, - 'sentry.sdk.version': { type: 'string', value: expect.any(String) }, - 'sentry.segment.id': { type: 'string', value: expect.stringMatching(/^[\da-f]{16}$/) }, - 'sentry.segment.name': { type: 'string', value: 'Test Span' }, - [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, - }; - - function expectedDbSpan({ - operation, - statement, - status = 'ok', - errorMessage, - }: { - operation: string; - statement: string; - status?: string; - errorMessage?: string; - }): unknown { - return { - attributes: { - ...COMMON_ATTRIBUTES, - 'db.operation.name': { type: 'string', value: operation }, - 'db.query.text': { type: 'string', value: statement }, - ...(errorMessage - ? { - 'error.type': { type: 'string', value: 'ReplyError' }, - 'sentry.status.message': { type: 'string', value: errorMessage }, - } - : {}), - }, - name: `${operation} localhost:6380`, - end_timestamp: expect.any(Number), - is_segment: false, - parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), - span_id: expect.stringMatching(/^[\da-f]{16}$/), - start_timestamp: expect.any(Number), - status, - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - }; - } - - createEsmAndCjsTests(__dirname, 'scenario-ioredis.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('should auto-instrument `ioredis` package with span streaming enabled', { timeout: 75_000 }, async () => { await createTestRunner() - .withEnv({ STREAMED: 'true' }) .expect({ - span: (container: SerializedStreamedSpanContainer) => { + span: container => { const segmentSpan = container.items.find(item => item.is_segment); expect(segmentSpan?.name).toBe('Test Span'); @@ -153,7 +112,7 @@ describeWithDockerCompose('redis auto instrumentation', { workingDirectory: [__d }) .start() .completed(); - }); - }); + }, + ); }); });