Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions MIGRATION.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -923,6 +923,7 @@ The following span names were adjusted:
| `queue.publish` | Integration-specific (`publish my-exchange`, `send my-topic`) | The messaging operation type and the destination (`send my-exchange`), or just the operation type when the destination has no name (`send`) |
| `queue.process` | Integration-specific, sometimes containing per-message data (`my-queue process`, `order.created.12345 process`) | The messaging operation type and the destination (`process my-exchange`), or just the operation type when the destination has no name (`process`) |
| `queue.receive` | The kafkajs operation name (`poll my-topic`) | The messaging operation type and the destination (`receive my-topic`) |
| `cache.get`, `cache.put`, `cache.remove` | The cache key(s) (`user:123`), or for dataloader the operation and loader name (`dataloader.load usersLoader`) | The cache operation (`cache.get`, `cache.put`, `cache.remove`) |
| `db` (mongoose) | `mongoose.<Model>.<operation>` (`mongoose.BlogPost.findOne`) | The operation and the collection (`findOne blogposts`), the database namespace when there is no collection, or `mongodb` when the SDK has neither |

`navigation.redirect` spans are started through the same code path as navigation spans, so they get the same names.
Expand DownExpand Up@@ -959,6 +960,10 @@ A mongoose span's name is built from `db.collection.name`, so it holds the colle

Messaging span names now read `<operation type> <destination>` in every integration. The amqplib, kafkajs and NestJS BullMQ integrations used their own word order or verb, so their names change: `my-queue process` became `process my-queue`, amqplib's `publish` became `send`, and the kafkajs batch span's `poll` became `receive`. Cloudflare Queues and the kafkajs producer already matched the conventions, so their names are the same in both trace lifecycles. The operation name an integration reports upstream stays on `messaging.operation.name`.

Cache keys are unbounded, so they are no longer part of a cache span name. They remain available on the `cache.key` attribute, and every cache span now also carries a `cache.operation` attribute (`get`, `put`, `remove`) — the value the name is built from. That attribute is set in both trace lifecycles. This affects the redis/ioredis cache spans (`cachePrefixes`), the Nuxt and Nitro storage spans, and the dataloader spans.

A dataloader span no longer carries the loader's `name` either (`dataloader.load usersLoader` becomes `cache.get`), because the cache conventions have no slot for it in the name. It is reported on the `db.collection.name` attribute instead — a loader batches one entity type, so it is the closest thing dataloader has to a collection — and that attribute is set in both trace lifecycles. Unnamed loaders do not set it.

AWS SQS `SendMessage`, `SendMessageBatch` and `ReceiveMessage`, and SNS `Publish`, are messaging spans (e.g. `queue.publish`) rather than `rpc` ones now. Every other command on those clients, such as `DeleteMessage`, stays `rpc`. Their names follow the messaging conventions too, so the operation comes first (`my-queue receive` becomes `receive my-queue`, `my-topic send` becomes `send my-topic`). A streamed SNS `Publish` to a platform endpoint is named `send`, because the endpoint ARN it used to carry ends in a per-device id (`endpoint/GCM/myapp/<uuid> send`). The full ARN remains on `messaging.destination.name`.

An amqplib span's destination is the exchange it uses, or the routing key when it uses the default exchange. RabbitMQ binds every queue to the default exchange under a key equal to the queue's own name, so `sendToQueue` spans are named after their queue (`send my-queue`) instead of dropping the destination. `messaging.destination.name` reports the same value, and the routing key remains on `messaging.rabbitmq.destination.routing_key` in full.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,6 +47,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheMissSpan) {
expect(cacheMissSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: false },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -64,6 +65,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheHitSpan) {
expect(cacheHitSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -81,6 +83,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheSetSpan) {
expect(cacheSetSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: expect.stringMatching(/^(cache)?$/) },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,35 +38,38 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(setSpan).toBeDefined();
expect(setSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(setSpan?.name).toBe(prefixKey('alias:user'));
expect(setSpan?.name).toBe('cache.put');

// Test get (alias for getItem)
expect(findSpansByMethod('getItem').length).toBeGreaterThanOrEqual(1);
const getSpan = findByKey('getItem', prefixKey('alias:user'));
expect(getSpan).toBeDefined();
expect(getSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getSpan?.name).toBe(prefixKey('alias:user'));
expect(getSpan?.name).toBe('cache.get');

// Test has (alias for hasItem)
expect(findSpansByMethod('hasItem').length).toBeGreaterThanOrEqual(1);
const hasSpan = findByKey('hasItem', prefixKey('alias:user'));
expect(hasSpan).toBeDefined();
expect(hasSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -82,25 +85,27 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(delSpan).toBeDefined();
expect(delSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(delSpan?.name).toBe(prefixKey('alias:temp1'));
expect(delSpan?.name).toBe('cache.remove');

const removeSpan = findByKey('removeItem', prefixKey('alias:temp2'));
expect(removeSpan).toBeDefined();
expect(removeSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp2') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(removeSpan?.name).toBe(prefixKey('alias:temp2'));
expect(removeSpan?.name).toBe('cache.remove');

// Verify all spans have OK status
expect(allStorageSpans.length).toBeGreaterThan(0);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,14 +38,15 @@ test.describe('Storage Instrumentation', () => {
expect(setItemSpan).toBeDefined();
expect(setItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});

expect(setItemSpan?.name).toBe(prefixKey('user:123'));
expect(setItemSpan?.name).toBe('cache.put');

// Test setItemRaw spans
expect(findSpansByMethod('setItemRaw').length).toBeGreaterThanOrEqual(1);
Expand All@@ -54,6 +55,7 @@ test.describe('Storage Instrumentation', () => {
expect(setItemRawSpan).toBeDefined();
expect(setItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
'db.operation.name': { type: 'string', value: 'setItemRaw' },
Expand All@@ -67,6 +69,7 @@ test.describe('Storage Instrumentation', () => {
expect(hasItemSpan).toBeDefined();
expect(hasItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -81,21 +84,23 @@ test.describe('Storage Instrumentation', () => {
expect(getItemSpan).toBeDefined();
expect(getItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getItemSpan?.name).toBe(prefixKey('user:123'));
expect(getItemSpan?.name).toBe('cache.get');

// Test getItemRaw spans - should have cache hit attribute
expect(findSpansByMethod('getItemRaw').length).toBeGreaterThanOrEqual(1);
const getItemRawSpan = findSpanByCacheKey('getItemRaw', prefixKey('raw:data'));
expect(getItemRawSpan).toBeDefined();
expect(getItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -109,6 +114,7 @@ test.describe('Storage Instrumentation', () => {
expect(getKeysSpans.length).toBeGreaterThanOrEqual(1);
expect(getKeysSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'getKeys' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand All@@ -121,6 +127,7 @@ test.describe('Storage Instrumentation', () => {
expect(removeItemSpan).toBeDefined();
expect(removeItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('batch:1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
Expand All@@ -133,6 +140,7 @@ test.describe('Storage Instrumentation', () => {
expect(clearSpans.length).toBeGreaterThanOrEqual(1);
expect(clearSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'clear' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import type { SerializedStreamedSpanContainer } from '@sentry/core';
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

Expand DownExpand Up@@ -103,6 +104,7 @@ describe('dataloader auto-instrumentation', () => {
expect(namedLoadSpan?.op).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.origin).toBe(ORIGIN);
expect(namedLoadSpan?.status).toBe('ok');
expect(namedLoadSpan?.data?.['db.collection.name']).toBe('usersLoader');
},
})
.start();
Expand All@@ -113,5 +115,37 @@ describe('dataloader auto-instrumentation', () => {
await runner.makeRequest('get', '/named');
await runner.completed();
}, 30_000);

test('names spans after the cache operation when streamed', async () => {
const runner = createRunner()
.withEnv({ STREAMED: 'true' })
.expect({
span: (container: SerializedStreamedSpanContainer) => {
const namedLoadSpan = container.items.find(
span => span.attributes?.['db.operation.name']?.value === 'load',
);
expect(namedLoadSpan?.name).toBe('cache.get');
expect(namedLoadSpan?.attributes?.['sentry.op']?.value).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.attributes?.['cache.operation']?.value).toBe('get');
// The loader name is no longer part of the span name, it moved to `db.collection.name`.
expect(namedLoadSpan?.attributes?.['db.collection.name']?.value).toBe('usersLoader');
},
})
.expect({
span: (container: SerializedStreamedSpanContainer) => {
for (const [operation, op] of Object.entries(CACHE_MUTATION_OPS)) {
const span = container.items.find(item => item.attributes?.['db.operation.name']?.value === operation);
expect(span, `expected a ${operation} span`).toBeDefined();
expect(span?.name).toBe(op);
expect(span?.attributes?.['sentry.op']?.value).toBe(op);
}
},
})
.start();

await runner.makeRequest('get', '/named');
await runner.makeRequest('get', '/cache-ops');
await runner.completed();
}, 30_000);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions MIGRATION.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -923,6 +923,7 @@ The following span names were adjusted:
| `queue.publish` | Integration-specific (`publish my-exchange`, `send my-topic`) | The messaging operation type and the destination (`send my-exchange`), or just the operation type when the destination has no name (`send`) |
| `queue.process` | Integration-specific, sometimes containing per-message data (`my-queue process`, `order.created.12345 process`) | The messaging operation type and the destination (`process my-exchange`), or just the operation type when the destination has no name (`process`) |
| `queue.receive` | The kafkajs operation name (`poll my-topic`) | The messaging operation type and the destination (`receive my-topic`) |
| `cache.get`, `cache.put`, `cache.remove` | The cache key(s) (`user:123`), or for dataloader the operation and loader name (`dataloader.load usersLoader`) | The cache operation (`cache.get`, `cache.put`, `cache.remove`) |
| `db` (mongoose) | `mongoose.<Model>.<operation>` (`mongoose.BlogPost.findOne`) | The operation and the collection (`findOne blogposts`), the database namespace when there is no collection, or `mongodb` when the SDK has neither |

`navigation.redirect` spans are started through the same code path as navigation spans, so they get the same names.
Expand DownExpand Up@@ -959,6 +960,10 @@ A mongoose span's name is built from `db.collection.name`, so it holds the colle

Messaging span names now read `<operation type> <destination>` in every integration. The amqplib, kafkajs and NestJS BullMQ integrations used their own word order or verb, so their names change: `my-queue process` became `process my-queue`, amqplib's `publish` became `send`, and the kafkajs batch span's `poll` became `receive`. Cloudflare Queues and the kafkajs producer already matched the conventions, so their names are the same in both trace lifecycles. The operation name an integration reports upstream stays on `messaging.operation.name`.

Cache keys are unbounded, so they are no longer part of a cache span name. They remain available on the `cache.key` attribute, and every cache span now also carries a `cache.operation` attribute (`get`, `put`, `remove`) — the value the name is built from. That attribute is set in both trace lifecycles. This affects the redis/ioredis cache spans (`cachePrefixes`), the Nuxt and Nitro storage spans, and the dataloader spans.

A dataloader span no longer carries the loader's `name` either (`dataloader.load usersLoader` becomes `cache.get`), because the cache conventions have no slot for it in the name. It is reported on the `db.collection.name` attribute instead — a loader batches one entity type, so it is the closest thing dataloader has to a collection — and that attribute is set in both trace lifecycles. Unnamed loaders do not set it.

AWS SQS `SendMessage`, `SendMessageBatch` and `ReceiveMessage`, and SNS `Publish`, are messaging spans (e.g. `queue.publish`) rather than `rpc` ones now. Every other command on those clients, such as `DeleteMessage`, stays `rpc`. Their names follow the messaging conventions too, so the operation comes first (`my-queue receive` becomes `receive my-queue`, `my-topic send` becomes `send my-topic`). A streamed SNS `Publish` to a platform endpoint is named `send`, because the endpoint ARN it used to carry ends in a per-device id (`endpoint/GCM/myapp/<uuid> send`). The full ARN remains on `messaging.destination.name`.

An amqplib span's destination is the exchange it uses, or the routing key when it uses the default exchange. RabbitMQ binds every queue to the default exchange under a key equal to the queue's own name, so `sendToQueue` spans are named after their queue (`send my-queue`) instead of dropping the destination. `messaging.destination.name` reports the same value, and the routing key remains on `messaging.rabbitmq.destination.routing_key` in full.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,6 +47,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheMissSpan) {
expect(cacheMissSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: false },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -64,6 +65,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheHitSpan) {
expect(cacheHitSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -81,6 +83,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheSetSpan) {
expect(cacheSetSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: expect.stringMatching(/^(cache)?$/) },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,35 +38,38 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(setSpan).toBeDefined();
expect(setSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(setSpan?.name).toBe(prefixKey('alias:user'));
expect(setSpan?.name).toBe('cache.put');

// Test get (alias for getItem)
expect(findSpansByMethod('getItem').length).toBeGreaterThanOrEqual(1);
const getSpan = findByKey('getItem', prefixKey('alias:user'));
expect(getSpan).toBeDefined();
expect(getSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getSpan?.name).toBe(prefixKey('alias:user'));
expect(getSpan?.name).toBe('cache.get');

// Test has (alias for hasItem)
expect(findSpansByMethod('hasItem').length).toBeGreaterThanOrEqual(1);
const hasSpan = findByKey('hasItem', prefixKey('alias:user'));
expect(hasSpan).toBeDefined();
expect(hasSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -82,25 +85,27 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(delSpan).toBeDefined();
expect(delSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(delSpan?.name).toBe(prefixKey('alias:temp1'));
expect(delSpan?.name).toBe('cache.remove');

const removeSpan = findByKey('removeItem', prefixKey('alias:temp2'));
expect(removeSpan).toBeDefined();
expect(removeSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp2') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(removeSpan?.name).toBe(prefixKey('alias:temp2'));
expect(removeSpan?.name).toBe('cache.remove');

// Verify all spans have OK status
expect(allStorageSpans.length).toBeGreaterThan(0);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,14 +38,15 @@ test.describe('Storage Instrumentation', () => {
expect(setItemSpan).toBeDefined();
expect(setItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});

expect(setItemSpan?.name).toBe(prefixKey('user:123'));
expect(setItemSpan?.name).toBe('cache.put');

// Test setItemRaw spans
expect(findSpansByMethod('setItemRaw').length).toBeGreaterThanOrEqual(1);
Expand All@@ -54,6 +55,7 @@ test.describe('Storage Instrumentation', () => {
expect(setItemRawSpan).toBeDefined();
expect(setItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
'db.operation.name': { type: 'string', value: 'setItemRaw' },
Expand All@@ -67,6 +69,7 @@ test.describe('Storage Instrumentation', () => {
expect(hasItemSpan).toBeDefined();
expect(hasItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -81,21 +84,23 @@ test.describe('Storage Instrumentation', () => {
expect(getItemSpan).toBeDefined();
expect(getItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getItemSpan?.name).toBe(prefixKey('user:123'));
expect(getItemSpan?.name).toBe('cache.get');

// Test getItemRaw spans - should have cache hit attribute
expect(findSpansByMethod('getItemRaw').length).toBeGreaterThanOrEqual(1);
const getItemRawSpan = findSpanByCacheKey('getItemRaw', prefixKey('raw:data'));
expect(getItemRawSpan).toBeDefined();
expect(getItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -109,6 +114,7 @@ test.describe('Storage Instrumentation', () => {
expect(getKeysSpans.length).toBeGreaterThanOrEqual(1);
expect(getKeysSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'getKeys' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand All@@ -121,6 +127,7 @@ test.describe('Storage Instrumentation', () => {
expect(removeItemSpan).toBeDefined();
expect(removeItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('batch:1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
Expand All@@ -133,6 +140,7 @@ test.describe('Storage Instrumentation', () => {
expect(clearSpans.length).toBeGreaterThanOrEqual(1);
expect(clearSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'clear' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import type { SerializedStreamedSpanContainer } from '@sentry/core';
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

Expand DownExpand Up@@ -103,6 +104,7 @@ describe('dataloader auto-instrumentation', () => {
expect(namedLoadSpan?.op).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.origin).toBe(ORIGIN);
expect(namedLoadSpan?.status).toBe('ok');
expect(namedLoadSpan?.data?.['db.collection.name']).toBe('usersLoader');
},
})
.start();
Expand All@@ -113,5 +115,37 @@ describe('dataloader auto-instrumentation', () => {
await runner.makeRequest('get', '/named');
await runner.completed();
}, 30_000);

test('names spans after the cache operation when streamed', async () => {
const runner = createRunner()
.withEnv({ STREAMED: 'true' })
.expect({
span: (container: SerializedStreamedSpanContainer) => {
const namedLoadSpan = container.items.find(
span => span.attributes?.['db.operation.name']?.value === 'load',
);
expect(namedLoadSpan?.name).toBe('cache.get');
expect(namedLoadSpan?.attributes?.['sentry.op']?.value).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.attributes?.['cache.operation']?.value).toBe('get');
// The loader name is no longer part of the span name, it moved to `db.collection.name`.
expect(namedLoadSpan?.attributes?.['db.collection.name']?.value).toBe('usersLoader');
},
})
.expect({
span: (container: SerializedStreamedSpanContainer) => {
for (const [operation, op] of Object.entries(CACHE_MUTATION_OPS)) {
const span = container.items.find(item => item.attributes?.['db.operation.name']?.value === operation);
expect(span, `expected a ${operation} span`).toBeDefined();
expect(span?.name).toBe(op);
expect(span?.attributes?.['sentry.op']?.value).toBe(op);
}
},
})
.start();

await runner.makeRequest('get', '/named');
await runner.makeRequest('get', '/cache-ops');
await runner.completed();
}, 30_000);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions MIGRATION.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -923,6 +923,7 @@ The following span names were adjusted:
| `queue.publish` | Integration-specific (`publish my-exchange`, `send my-topic`) | The messaging operation type and the destination (`send my-exchange`), or just the operation type when the destination has no name (`send`) |
| `queue.process` | Integration-specific, sometimes containing per-message data (`my-queue process`, `order.created.12345 process`) | The messaging operation type and the destination (`process my-exchange`), or just the operation type when the destination has no name (`process`) |
| `queue.receive` | The kafkajs operation name (`poll my-topic`) | The messaging operation type and the destination (`receive my-topic`) |
| `cache.get`, `cache.put`, `cache.remove` | The cache key(s) (`user:123`), or for dataloader the operation and loader name (`dataloader.load usersLoader`) | The cache operation (`cache.get`, `cache.put`, `cache.remove`) |
| `db` (mongoose) | `mongoose.<Model>.<operation>` (`mongoose.BlogPost.findOne`) | The operation and the collection (`findOne blogposts`), the database namespace when there is no collection, or `mongodb` when the SDK has neither |

`navigation.redirect` spans are started through the same code path as navigation spans, so they get the same names.
Expand DownExpand Up@@ -959,6 +960,10 @@ A mongoose span's name is built from `db.collection.name`, so it holds the colle

Messaging span names now read `<operation type> <destination>` in every integration. The amqplib, kafkajs and NestJS BullMQ integrations used their own word order or verb, so their names change: `my-queue process` became `process my-queue`, amqplib's `publish` became `send`, and the kafkajs batch span's `poll` became `receive`. Cloudflare Queues and the kafkajs producer already matched the conventions, so their names are the same in both trace lifecycles. The operation name an integration reports upstream stays on `messaging.operation.name`.

Cache keys are unbounded, so they are no longer part of a cache span name. They remain available on the `cache.key` attribute, and every cache span now also carries a `cache.operation` attribute (`get`, `put`, `remove`) — the value the name is built from. That attribute is set in both trace lifecycles. This affects the redis/ioredis cache spans (`cachePrefixes`), the Nuxt and Nitro storage spans, and the dataloader spans.

A dataloader span no longer carries the loader's `name` either (`dataloader.load usersLoader` becomes `cache.get`), because the cache conventions have no slot for it in the name. It is reported on the `db.collection.name` attribute instead — a loader batches one entity type, so it is the closest thing dataloader has to a collection — and that attribute is set in both trace lifecycles. Unnamed loaders do not set it.

AWS SQS `SendMessage`, `SendMessageBatch` and `ReceiveMessage`, and SNS `Publish`, are messaging spans (e.g. `queue.publish`) rather than `rpc` ones now. Every other command on those clients, such as `DeleteMessage`, stays `rpc`. Their names follow the messaging conventions too, so the operation comes first (`my-queue receive` becomes `receive my-queue`, `my-topic send` becomes `send my-topic`). A streamed SNS `Publish` to a platform endpoint is named `send`, because the endpoint ARN it used to carry ends in a per-device id (`endpoint/GCM/myapp/<uuid> send`). The full ARN remains on `messaging.destination.name`.

An amqplib span's destination is the exchange it uses, or the routing key when it uses the default exchange. RabbitMQ binds every queue to the default exchange under a key equal to the queue's own name, so `sendToQueue` spans are named after their queue (`send my-queue`) instead of dropping the destination. `messaging.destination.name` reports the same value, and the routing key remains on `messaging.rabbitmq.destination.routing_key` in full.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,6 +47,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheMissSpan) {
expect(cacheMissSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: false },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -64,6 +65,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheHitSpan) {
expect(cacheHitSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -81,6 +83,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheSetSpan) {
expect(cacheSetSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: expect.stringMatching(/^(cache)?$/) },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,35 +38,38 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(setSpan).toBeDefined();
expect(setSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(setSpan?.name).toBe(prefixKey('alias:user'));
expect(setSpan?.name).toBe('cache.put');

// Test get (alias for getItem)
expect(findSpansByMethod('getItem').length).toBeGreaterThanOrEqual(1);
const getSpan = findByKey('getItem', prefixKey('alias:user'));
expect(getSpan).toBeDefined();
expect(getSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getSpan?.name).toBe(prefixKey('alias:user'));
expect(getSpan?.name).toBe('cache.get');

// Test has (alias for hasItem)
expect(findSpansByMethod('hasItem').length).toBeGreaterThanOrEqual(1);
const hasSpan = findByKey('hasItem', prefixKey('alias:user'));
expect(hasSpan).toBeDefined();
expect(hasSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -82,25 +85,27 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(delSpan).toBeDefined();
expect(delSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(delSpan?.name).toBe(prefixKey('alias:temp1'));
expect(delSpan?.name).toBe('cache.remove');

const removeSpan = findByKey('removeItem', prefixKey('alias:temp2'));
expect(removeSpan).toBeDefined();
expect(removeSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp2') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(removeSpan?.name).toBe(prefixKey('alias:temp2'));
expect(removeSpan?.name).toBe('cache.remove');

// Verify all spans have OK status
expect(allStorageSpans.length).toBeGreaterThan(0);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,14 +38,15 @@ test.describe('Storage Instrumentation', () => {
expect(setItemSpan).toBeDefined();
expect(setItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});

expect(setItemSpan?.name).toBe(prefixKey('user:123'));
expect(setItemSpan?.name).toBe('cache.put');

// Test setItemRaw spans
expect(findSpansByMethod('setItemRaw').length).toBeGreaterThanOrEqual(1);
Expand All@@ -54,6 +55,7 @@ test.describe('Storage Instrumentation', () => {
expect(setItemRawSpan).toBeDefined();
expect(setItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
'db.operation.name': { type: 'string', value: 'setItemRaw' },
Expand All@@ -67,6 +69,7 @@ test.describe('Storage Instrumentation', () => {
expect(hasItemSpan).toBeDefined();
expect(hasItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -81,21 +84,23 @@ test.describe('Storage Instrumentation', () => {
expect(getItemSpan).toBeDefined();
expect(getItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getItemSpan?.name).toBe(prefixKey('user:123'));
expect(getItemSpan?.name).toBe('cache.get');

// Test getItemRaw spans - should have cache hit attribute
expect(findSpansByMethod('getItemRaw').length).toBeGreaterThanOrEqual(1);
const getItemRawSpan = findSpanByCacheKey('getItemRaw', prefixKey('raw:data'));
expect(getItemRawSpan).toBeDefined();
expect(getItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -109,6 +114,7 @@ test.describe('Storage Instrumentation', () => {
expect(getKeysSpans.length).toBeGreaterThanOrEqual(1);
expect(getKeysSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'getKeys' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand All@@ -121,6 +127,7 @@ test.describe('Storage Instrumentation', () => {
expect(removeItemSpan).toBeDefined();
expect(removeItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('batch:1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
Expand All@@ -133,6 +140,7 @@ test.describe('Storage Instrumentation', () => {
expect(clearSpans.length).toBeGreaterThanOrEqual(1);
expect(clearSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'clear' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import type { SerializedStreamedSpanContainer } from '@sentry/core';
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

Expand DownExpand Up@@ -103,6 +104,7 @@ describe('dataloader auto-instrumentation', () => {
expect(namedLoadSpan?.op).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.origin).toBe(ORIGIN);
expect(namedLoadSpan?.status).toBe('ok');
expect(namedLoadSpan?.data?.['db.collection.name']).toBe('usersLoader');
},
})
.start();
Expand All@@ -113,5 +115,37 @@ describe('dataloader auto-instrumentation', () => {
await runner.makeRequest('get', '/named');
await runner.completed();
}, 30_000);

test('names spans after the cache operation when streamed', async () => {
const runner = createRunner()
.withEnv({ STREAMED: 'true' })
.expect({
span: (container: SerializedStreamedSpanContainer) => {
const namedLoadSpan = container.items.find(
span => span.attributes?.['db.operation.name']?.value === 'load',
);
expect(namedLoadSpan?.name).toBe('cache.get');
expect(namedLoadSpan?.attributes?.['sentry.op']?.value).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.attributes?.['cache.operation']?.value).toBe('get');
// The loader name is no longer part of the span name, it moved to `db.collection.name`.
expect(namedLoadSpan?.attributes?.['db.collection.name']?.value).toBe('usersLoader');
},
})
.expect({
span: (container: SerializedStreamedSpanContainer) => {
for (const [operation, op] of Object.entries(CACHE_MUTATION_OPS)) {
const span = container.items.find(item => item.attributes?.['db.operation.name']?.value === operation);
expect(span, `expected a ${operation} span`).toBeDefined();
expect(span?.name).toBe(op);
expect(span?.attributes?.['sentry.op']?.value).toBe(op);
}
},
})
.start();

await runner.makeRequest('get', '/named');
await runner.makeRequest('get', '/cache-ops');
await runner.completed();
}, 30_000);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions MIGRATION.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -923,6 +923,7 @@ The following span names were adjusted:
| `queue.publish` | Integration-specific (`publish my-exchange`, `send my-topic`) | The messaging operation type and the destination (`send my-exchange`), or just the operation type when the destination has no name (`send`) |
| `queue.process` | Integration-specific, sometimes containing per-message data (`my-queue process`, `order.created.12345 process`) | The messaging operation type and the destination (`process my-exchange`), or just the operation type when the destination has no name (`process`) |
| `queue.receive` | The kafkajs operation name (`poll my-topic`) | The messaging operation type and the destination (`receive my-topic`) |
| `cache.get`, `cache.put`, `cache.remove` | The cache key(s) (`user:123`), or for dataloader the operation and loader name (`dataloader.load usersLoader`) | The cache operation (`cache.get`, `cache.put`, `cache.remove`) |
| `db` (mongoose) | `mongoose.<Model>.<operation>` (`mongoose.BlogPost.findOne`) | The operation and the collection (`findOne blogposts`), the database namespace when there is no collection, or `mongodb` when the SDK has neither |

`navigation.redirect` spans are started through the same code path as navigation spans, so they get the same names.
Expand DownExpand Up@@ -959,6 +960,10 @@ A mongoose span's name is built from `db.collection.name`, so it holds the colle

Messaging span names now read `<operation type> <destination>` in every integration. The amqplib, kafkajs and NestJS BullMQ integrations used their own word order or verb, so their names change: `my-queue process` became `process my-queue`, amqplib's `publish` became `send`, and the kafkajs batch span's `poll` became `receive`. Cloudflare Queues and the kafkajs producer already matched the conventions, so their names are the same in both trace lifecycles. The operation name an integration reports upstream stays on `messaging.operation.name`.

Cache keys are unbounded, so they are no longer part of a cache span name. They remain available on the `cache.key` attribute, and every cache span now also carries a `cache.operation` attribute (`get`, `put`, `remove`) — the value the name is built from. That attribute is set in both trace lifecycles. This affects the redis/ioredis cache spans (`cachePrefixes`), the Nuxt and Nitro storage spans, and the dataloader spans.

A dataloader span no longer carries the loader's `name` either (`dataloader.load usersLoader` becomes `cache.get`), because the cache conventions have no slot for it in the name. It is reported on the `db.collection.name` attribute instead — a loader batches one entity type, so it is the closest thing dataloader has to a collection — and that attribute is set in both trace lifecycles. Unnamed loaders do not set it.

AWS SQS `SendMessage`, `SendMessageBatch` and `ReceiveMessage`, and SNS `Publish`, are messaging spans (e.g. `queue.publish`) rather than `rpc` ones now. Every other command on those clients, such as `DeleteMessage`, stays `rpc`. Their names follow the messaging conventions too, so the operation comes first (`my-queue receive` becomes `receive my-queue`, `my-topic send` becomes `send my-topic`). A streamed SNS `Publish` to a platform endpoint is named `send`, because the endpoint ARN it used to carry ends in a per-device id (`endpoint/GCM/myapp/<uuid> send`). The full ARN remains on `messaging.destination.name`.

An amqplib span's destination is the exchange it uses, or the routing key when it uses the default exchange. RabbitMQ binds every queue to the default exchange under a key equal to the queue's own name, so `sendToQueue` spans are named after their queue (`send my-queue`) instead of dropping the destination. `messaging.destination.name` reports the same value, and the routing key remains on `messaging.rabbitmq.destination.routing_key` in full.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,6 +47,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheMissSpan) {
expect(cacheMissSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: false },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -64,6 +65,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheHitSpan) {
expect(cacheHitSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -81,6 +83,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheSetSpan) {
expect(cacheSetSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: expect.stringMatching(/^(cache)?$/) },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,35 +38,38 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(setSpan).toBeDefined();
expect(setSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(setSpan?.name).toBe(prefixKey('alias:user'));
expect(setSpan?.name).toBe('cache.put');

// Test get (alias for getItem)
expect(findSpansByMethod('getItem').length).toBeGreaterThanOrEqual(1);
const getSpan = findByKey('getItem', prefixKey('alias:user'));
expect(getSpan).toBeDefined();
expect(getSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getSpan?.name).toBe(prefixKey('alias:user'));
expect(getSpan?.name).toBe('cache.get');

// Test has (alias for hasItem)
expect(findSpansByMethod('hasItem').length).toBeGreaterThanOrEqual(1);
const hasSpan = findByKey('hasItem', prefixKey('alias:user'));
expect(hasSpan).toBeDefined();
expect(hasSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -82,25 +85,27 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(delSpan).toBeDefined();
expect(delSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(delSpan?.name).toBe(prefixKey('alias:temp1'));
expect(delSpan?.name).toBe('cache.remove');

const removeSpan = findByKey('removeItem', prefixKey('alias:temp2'));
expect(removeSpan).toBeDefined();
expect(removeSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp2') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(removeSpan?.name).toBe(prefixKey('alias:temp2'));
expect(removeSpan?.name).toBe('cache.remove');

// Verify all spans have OK status
expect(allStorageSpans.length).toBeGreaterThan(0);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,14 +38,15 @@ test.describe('Storage Instrumentation', () => {
expect(setItemSpan).toBeDefined();
expect(setItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});

expect(setItemSpan?.name).toBe(prefixKey('user:123'));
expect(setItemSpan?.name).toBe('cache.put');

// Test setItemRaw spans
expect(findSpansByMethod('setItemRaw').length).toBeGreaterThanOrEqual(1);
Expand All@@ -54,6 +55,7 @@ test.describe('Storage Instrumentation', () => {
expect(setItemRawSpan).toBeDefined();
expect(setItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
'db.operation.name': { type: 'string', value: 'setItemRaw' },
Expand All@@ -67,6 +69,7 @@ test.describe('Storage Instrumentation', () => {
expect(hasItemSpan).toBeDefined();
expect(hasItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -81,21 +84,23 @@ test.describe('Storage Instrumentation', () => {
expect(getItemSpan).toBeDefined();
expect(getItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getItemSpan?.name).toBe(prefixKey('user:123'));
expect(getItemSpan?.name).toBe('cache.get');

// Test getItemRaw spans - should have cache hit attribute
expect(findSpansByMethod('getItemRaw').length).toBeGreaterThanOrEqual(1);
const getItemRawSpan = findSpanByCacheKey('getItemRaw', prefixKey('raw:data'));
expect(getItemRawSpan).toBeDefined();
expect(getItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -109,6 +114,7 @@ test.describe('Storage Instrumentation', () => {
expect(getKeysSpans.length).toBeGreaterThanOrEqual(1);
expect(getKeysSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'getKeys' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand All@@ -121,6 +127,7 @@ test.describe('Storage Instrumentation', () => {
expect(removeItemSpan).toBeDefined();
expect(removeItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('batch:1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
Expand All@@ -133,6 +140,7 @@ test.describe('Storage Instrumentation', () => {
expect(clearSpans.length).toBeGreaterThanOrEqual(1);
expect(clearSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'clear' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import type { SerializedStreamedSpanContainer } from '@sentry/core';
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

Expand DownExpand Up@@ -103,6 +104,7 @@ describe('dataloader auto-instrumentation', () => {
expect(namedLoadSpan?.op).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.origin).toBe(ORIGIN);
expect(namedLoadSpan?.status).toBe('ok');
expect(namedLoadSpan?.data?.['db.collection.name']).toBe('usersLoader');
},
})
.start();
Expand All@@ -113,5 +115,37 @@ describe('dataloader auto-instrumentation', () => {
await runner.makeRequest('get', '/named');
await runner.completed();
}, 30_000);

test('names spans after the cache operation when streamed', async () => {
const runner = createRunner()
.withEnv({ STREAMED: 'true' })
.expect({
span: (container: SerializedStreamedSpanContainer) => {
const namedLoadSpan = container.items.find(
span => span.attributes?.['db.operation.name']?.value === 'load',
);
expect(namedLoadSpan?.name).toBe('cache.get');
expect(namedLoadSpan?.attributes?.['sentry.op']?.value).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.attributes?.['cache.operation']?.value).toBe('get');
// The loader name is no longer part of the span name, it moved to `db.collection.name`.
expect(namedLoadSpan?.attributes?.['db.collection.name']?.value).toBe('usersLoader');
},
})
.expect({
span: (container: SerializedStreamedSpanContainer) => {
for (const [operation, op] of Object.entries(CACHE_MUTATION_OPS)) {
const span = container.items.find(item => item.attributes?.['db.operation.name']?.value === operation);
expect(span, `expected a ${operation} span`).toBeDefined();
expect(span?.name).toBe(op);
expect(span?.attributes?.['sentry.op']?.value).toBe(op);
}
},
})
.start();

await runner.makeRequest('get', '/named');
await runner.makeRequest('get', '/cache-ops');
await runner.completed();
}, 30_000);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions MIGRATION.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -923,6 +923,7 @@ The following span names were adjusted:
| `queue.publish` | Integration-specific (`publish my-exchange`, `send my-topic`) | The messaging operation type and the destination (`send my-exchange`), or just the operation type when the destination has no name (`send`) |
| `queue.process` | Integration-specific, sometimes containing per-message data (`my-queue process`, `order.created.12345 process`) | The messaging operation type and the destination (`process my-exchange`), or just the operation type when the destination has no name (`process`) |
| `queue.receive` | The kafkajs operation name (`poll my-topic`) | The messaging operation type and the destination (`receive my-topic`) |
| `cache.get`, `cache.put`, `cache.remove` | The cache key(s) (`user:123`), or for dataloader the operation and loader name (`dataloader.load usersLoader`) | The cache operation (`cache.get`, `cache.put`, `cache.remove`) |
| `db` (mongoose) | `mongoose.<Model>.<operation>` (`mongoose.BlogPost.findOne`) | The operation and the collection (`findOne blogposts`), the database namespace when there is no collection, or `mongodb` when the SDK has neither |

`navigation.redirect` spans are started through the same code path as navigation spans, so they get the same names.
Expand DownExpand Up@@ -959,6 +960,10 @@ A mongoose span's name is built from `db.collection.name`, so it holds the colle

Messaging span names now read `<operation type> <destination>` in every integration. The amqplib, kafkajs and NestJS BullMQ integrations used their own word order or verb, so their names change: `my-queue process` became `process my-queue`, amqplib's `publish` became `send`, and the kafkajs batch span's `poll` became `receive`. Cloudflare Queues and the kafkajs producer already matched the conventions, so their names are the same in both trace lifecycles. The operation name an integration reports upstream stays on `messaging.operation.name`.

Cache keys are unbounded, so they are no longer part of a cache span name. They remain available on the `cache.key` attribute, and every cache span now also carries a `cache.operation` attribute (`get`, `put`, `remove`) — the value the name is built from. That attribute is set in both trace lifecycles. This affects the redis/ioredis cache spans (`cachePrefixes`), the Nuxt and Nitro storage spans, and the dataloader spans.

A dataloader span no longer carries the loader's `name` either (`dataloader.load usersLoader` becomes `cache.get`), because the cache conventions have no slot for it in the name. It is reported on the `db.collection.name` attribute instead — a loader batches one entity type, so it is the closest thing dataloader has to a collection — and that attribute is set in both trace lifecycles. Unnamed loaders do not set it.

AWS SQS `SendMessage`, `SendMessageBatch` and `ReceiveMessage`, and SNS `Publish`, are messaging spans (e.g. `queue.publish`) rather than `rpc` ones now. Every other command on those clients, such as `DeleteMessage`, stays `rpc`. Their names follow the messaging conventions too, so the operation comes first (`my-queue receive` becomes `receive my-queue`, `my-topic send` becomes `send my-topic`). A streamed SNS `Publish` to a platform endpoint is named `send`, because the endpoint ARN it used to carry ends in a per-device id (`endpoint/GCM/myapp/<uuid> send`). The full ARN remains on `messaging.destination.name`.

An amqplib span's destination is the exchange it uses, or the routing key when it uses the default exchange. RabbitMQ binds every queue to the default exchange under a key equal to the queue's own name, so `sendToQueue` spans are named after their queue (`send my-queue`) instead of dropping the destination. `messaging.destination.name` reports the same value, and the routing key remains on `messaging.rabbitmq.destination.routing_key` in full.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,6 +47,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheMissSpan) {
expect(cacheMissSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: false },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -64,6 +65,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheHitSpan) {
expect(cacheHitSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -81,6 +83,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheSetSpan) {
expect(cacheSetSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: expect.stringMatching(/^(cache)?$/) },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,35 +38,38 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(setSpan).toBeDefined();
expect(setSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(setSpan?.name).toBe(prefixKey('alias:user'));
expect(setSpan?.name).toBe('cache.put');

// Test get (alias for getItem)
expect(findSpansByMethod('getItem').length).toBeGreaterThanOrEqual(1);
const getSpan = findByKey('getItem', prefixKey('alias:user'));
expect(getSpan).toBeDefined();
expect(getSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getSpan?.name).toBe(prefixKey('alias:user'));
expect(getSpan?.name).toBe('cache.get');

// Test has (alias for hasItem)
expect(findSpansByMethod('hasItem').length).toBeGreaterThanOrEqual(1);
const hasSpan = findByKey('hasItem', prefixKey('alias:user'));
expect(hasSpan).toBeDefined();
expect(hasSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -82,25 +85,27 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(delSpan).toBeDefined();
expect(delSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(delSpan?.name).toBe(prefixKey('alias:temp1'));
expect(delSpan?.name).toBe('cache.remove');

const removeSpan = findByKey('removeItem', prefixKey('alias:temp2'));
expect(removeSpan).toBeDefined();
expect(removeSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp2') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(removeSpan?.name).toBe(prefixKey('alias:temp2'));
expect(removeSpan?.name).toBe('cache.remove');

// Verify all spans have OK status
expect(allStorageSpans.length).toBeGreaterThan(0);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,14 +38,15 @@ test.describe('Storage Instrumentation', () => {
expect(setItemSpan).toBeDefined();
expect(setItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});

expect(setItemSpan?.name).toBe(prefixKey('user:123'));
expect(setItemSpan?.name).toBe('cache.put');

// Test setItemRaw spans
expect(findSpansByMethod('setItemRaw').length).toBeGreaterThanOrEqual(1);
Expand All@@ -54,6 +55,7 @@ test.describe('Storage Instrumentation', () => {
expect(setItemRawSpan).toBeDefined();
expect(setItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
'db.operation.name': { type: 'string', value: 'setItemRaw' },
Expand All@@ -67,6 +69,7 @@ test.describe('Storage Instrumentation', () => {
expect(hasItemSpan).toBeDefined();
expect(hasItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -81,21 +84,23 @@ test.describe('Storage Instrumentation', () => {
expect(getItemSpan).toBeDefined();
expect(getItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getItemSpan?.name).toBe(prefixKey('user:123'));
expect(getItemSpan?.name).toBe('cache.get');

// Test getItemRaw spans - should have cache hit attribute
expect(findSpansByMethod('getItemRaw').length).toBeGreaterThanOrEqual(1);
const getItemRawSpan = findSpanByCacheKey('getItemRaw', prefixKey('raw:data'));
expect(getItemRawSpan).toBeDefined();
expect(getItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -109,6 +114,7 @@ test.describe('Storage Instrumentation', () => {
expect(getKeysSpans.length).toBeGreaterThanOrEqual(1);
expect(getKeysSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'getKeys' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand All@@ -121,6 +127,7 @@ test.describe('Storage Instrumentation', () => {
expect(removeItemSpan).toBeDefined();
expect(removeItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('batch:1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
Expand All@@ -133,6 +140,7 @@ test.describe('Storage Instrumentation', () => {
expect(clearSpans.length).toBeGreaterThanOrEqual(1);
expect(clearSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'clear' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import type { SerializedStreamedSpanContainer } from '@sentry/core';
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

Expand DownExpand Up@@ -103,6 +104,7 @@ describe('dataloader auto-instrumentation', () => {
expect(namedLoadSpan?.op).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.origin).toBe(ORIGIN);
expect(namedLoadSpan?.status).toBe('ok');
expect(namedLoadSpan?.data?.['db.collection.name']).toBe('usersLoader');
},
})
.start();
Expand All@@ -113,5 +115,37 @@ describe('dataloader auto-instrumentation', () => {
await runner.makeRequest('get', '/named');
await runner.completed();
}, 30_000);

test('names spans after the cache operation when streamed', async () => {
const runner = createRunner()
.withEnv({ STREAMED: 'true' })
.expect({
span: (container: SerializedStreamedSpanContainer) => {
const namedLoadSpan = container.items.find(
span => span.attributes?.['db.operation.name']?.value === 'load',
);
expect(namedLoadSpan?.name).toBe('cache.get');
expect(namedLoadSpan?.attributes?.['sentry.op']?.value).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.attributes?.['cache.operation']?.value).toBe('get');
// The loader name is no longer part of the span name, it moved to `db.collection.name`.
expect(namedLoadSpan?.attributes?.['db.collection.name']?.value).toBe('usersLoader');
},
})
.expect({
span: (container: SerializedStreamedSpanContainer) => {
for (const [operation, op] of Object.entries(CACHE_MUTATION_OPS)) {
const span = container.items.find(item => item.attributes?.['db.operation.name']?.value === operation);
expect(span, `expected a ${operation} span`).toBeDefined();
expect(span?.name).toBe(op);
expect(span?.attributes?.['sentry.op']?.value).toBe(op);
}
},
})
.start();

await runner.makeRequest('get', '/named');
await runner.makeRequest('get', '/cache-ops');
await runner.completed();
}, 30_000);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions MIGRATION.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -923,6 +923,7 @@ The following span names were adjusted:
| `queue.publish` | Integration-specific (`publish my-exchange`, `send my-topic`) | The messaging operation type and the destination (`send my-exchange`), or just the operation type when the destination has no name (`send`) |
| `queue.process` | Integration-specific, sometimes containing per-message data (`my-queue process`, `order.created.12345 process`) | The messaging operation type and the destination (`process my-exchange`), or just the operation type when the destination has no name (`process`) |
| `queue.receive` | The kafkajs operation name (`poll my-topic`) | The messaging operation type and the destination (`receive my-topic`) |
| `cache.get`, `cache.put`, `cache.remove` | The cache key(s) (`user:123`), or for dataloader the operation and loader name (`dataloader.load usersLoader`) | The cache operation (`cache.get`, `cache.put`, `cache.remove`) |
| `db` (mongoose) | `mongoose.<Model>.<operation>` (`mongoose.BlogPost.findOne`) | The operation and the collection (`findOne blogposts`), the database namespace when there is no collection, or `mongodb` when the SDK has neither |

`navigation.redirect` spans are started through the same code path as navigation spans, so they get the same names.
Expand DownExpand Up@@ -959,6 +960,10 @@ A mongoose span's name is built from `db.collection.name`, so it holds the colle

Messaging span names now read `<operation type> <destination>` in every integration. The amqplib, kafkajs and NestJS BullMQ integrations used their own word order or verb, so their names change: `my-queue process` became `process my-queue`, amqplib's `publish` became `send`, and the kafkajs batch span's `poll` became `receive`. Cloudflare Queues and the kafkajs producer already matched the conventions, so their names are the same in both trace lifecycles. The operation name an integration reports upstream stays on `messaging.operation.name`.

Cache keys are unbounded, so they are no longer part of a cache span name. They remain available on the `cache.key` attribute, and every cache span now also carries a `cache.operation` attribute (`get`, `put`, `remove`) — the value the name is built from. That attribute is set in both trace lifecycles. This affects the redis/ioredis cache spans (`cachePrefixes`), the Nuxt and Nitro storage spans, and the dataloader spans.

A dataloader span no longer carries the loader's `name` either (`dataloader.load usersLoader` becomes `cache.get`), because the cache conventions have no slot for it in the name. It is reported on the `db.collection.name` attribute instead — a loader batches one entity type, so it is the closest thing dataloader has to a collection — and that attribute is set in both trace lifecycles. Unnamed loaders do not set it.

AWS SQS `SendMessage`, `SendMessageBatch` and `ReceiveMessage`, and SNS `Publish`, are messaging spans (e.g. `queue.publish`) rather than `rpc` ones now. Every other command on those clients, such as `DeleteMessage`, stays `rpc`. Their names follow the messaging conventions too, so the operation comes first (`my-queue receive` becomes `receive my-queue`, `my-topic send` becomes `send my-topic`). A streamed SNS `Publish` to a platform endpoint is named `send`, because the endpoint ARN it used to carry ends in a per-device id (`endpoint/GCM/myapp/<uuid> send`). The full ARN remains on `messaging.destination.name`.

An amqplib span's destination is the exchange it uses, or the routing key when it uses the default exchange. RabbitMQ binds every queue to the default exchange under a key equal to the queue's own name, so `sendToQueue` spans are named after their queue (`send my-queue`) instead of dropping the destination. `messaging.destination.name` reports the same value, and the routing key remains on `messaging.rabbitmq.destination.routing_key` in full.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,6 +47,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheMissSpan) {
expect(cacheMissSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: false },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -64,6 +65,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheHitSpan) {
expect(cacheHitSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -81,6 +83,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheSetSpan) {
expect(cacheSetSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: expect.stringMatching(/^(cache)?$/) },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,35 +38,38 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(setSpan).toBeDefined();
expect(setSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(setSpan?.name).toBe(prefixKey('alias:user'));
expect(setSpan?.name).toBe('cache.put');

// Test get (alias for getItem)
expect(findSpansByMethod('getItem').length).toBeGreaterThanOrEqual(1);
const getSpan = findByKey('getItem', prefixKey('alias:user'));
expect(getSpan).toBeDefined();
expect(getSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getSpan?.name).toBe(prefixKey('alias:user'));
expect(getSpan?.name).toBe('cache.get');

// Test has (alias for hasItem)
expect(findSpansByMethod('hasItem').length).toBeGreaterThanOrEqual(1);
const hasSpan = findByKey('hasItem', prefixKey('alias:user'));
expect(hasSpan).toBeDefined();
expect(hasSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -82,25 +85,27 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(delSpan).toBeDefined();
expect(delSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(delSpan?.name).toBe(prefixKey('alias:temp1'));
expect(delSpan?.name).toBe('cache.remove');

const removeSpan = findByKey('removeItem', prefixKey('alias:temp2'));
expect(removeSpan).toBeDefined();
expect(removeSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp2') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(removeSpan?.name).toBe(prefixKey('alias:temp2'));
expect(removeSpan?.name).toBe('cache.remove');

// Verify all spans have OK status
expect(allStorageSpans.length).toBeGreaterThan(0);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,14 +38,15 @@ test.describe('Storage Instrumentation', () => {
expect(setItemSpan).toBeDefined();
expect(setItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});

expect(setItemSpan?.name).toBe(prefixKey('user:123'));
expect(setItemSpan?.name).toBe('cache.put');

// Test setItemRaw spans
expect(findSpansByMethod('setItemRaw').length).toBeGreaterThanOrEqual(1);
Expand All@@ -54,6 +55,7 @@ test.describe('Storage Instrumentation', () => {
expect(setItemRawSpan).toBeDefined();
expect(setItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
'db.operation.name': { type: 'string', value: 'setItemRaw' },
Expand All@@ -67,6 +69,7 @@ test.describe('Storage Instrumentation', () => {
expect(hasItemSpan).toBeDefined();
expect(hasItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -81,21 +84,23 @@ test.describe('Storage Instrumentation', () => {
expect(getItemSpan).toBeDefined();
expect(getItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getItemSpan?.name).toBe(prefixKey('user:123'));
expect(getItemSpan?.name).toBe('cache.get');

// Test getItemRaw spans - should have cache hit attribute
expect(findSpansByMethod('getItemRaw').length).toBeGreaterThanOrEqual(1);
const getItemRawSpan = findSpanByCacheKey('getItemRaw', prefixKey('raw:data'));
expect(getItemRawSpan).toBeDefined();
expect(getItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -109,6 +114,7 @@ test.describe('Storage Instrumentation', () => {
expect(getKeysSpans.length).toBeGreaterThanOrEqual(1);
expect(getKeysSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'getKeys' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand All@@ -121,6 +127,7 @@ test.describe('Storage Instrumentation', () => {
expect(removeItemSpan).toBeDefined();
expect(removeItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('batch:1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
Expand All@@ -133,6 +140,7 @@ test.describe('Storage Instrumentation', () => {
expect(clearSpans.length).toBeGreaterThanOrEqual(1);
expect(clearSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'clear' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import type { SerializedStreamedSpanContainer } from '@sentry/core';
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

Expand DownExpand Up@@ -103,6 +104,7 @@ describe('dataloader auto-instrumentation', () => {
expect(namedLoadSpan?.op).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.origin).toBe(ORIGIN);
expect(namedLoadSpan?.status).toBe('ok');
expect(namedLoadSpan?.data?.['db.collection.name']).toBe('usersLoader');
},
})
.start();
Expand All@@ -113,5 +115,37 @@ describe('dataloader auto-instrumentation', () => {
await runner.makeRequest('get', '/named');
await runner.completed();
}, 30_000);

test('names spans after the cache operation when streamed', async () => {
const runner = createRunner()
.withEnv({ STREAMED: 'true' })
.expect({
span: (container: SerializedStreamedSpanContainer) => {
const namedLoadSpan = container.items.find(
span => span.attributes?.['db.operation.name']?.value === 'load',
);
expect(namedLoadSpan?.name).toBe('cache.get');
expect(namedLoadSpan?.attributes?.['sentry.op']?.value).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.attributes?.['cache.operation']?.value).toBe('get');
// The loader name is no longer part of the span name, it moved to `db.collection.name`.
expect(namedLoadSpan?.attributes?.['db.collection.name']?.value).toBe('usersLoader');
},
})
.expect({
span: (container: SerializedStreamedSpanContainer) => {
for (const [operation, op] of Object.entries(CACHE_MUTATION_OPS)) {
const span = container.items.find(item => item.attributes?.['db.operation.name']?.value === operation);
expect(span, `expected a ${operation} span`).toBeDefined();
expect(span?.name).toBe(op);
expect(span?.attributes?.['sentry.op']?.value).toBe(op);
}
},
})
.start();

await runner.makeRequest('get', '/named');
await runner.makeRequest('get', '/cache-ops');
await runner.completed();
}, 30_000);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions MIGRATION.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -923,6 +923,7 @@ The following span names were adjusted:
| `queue.publish` | Integration-specific (`publish my-exchange`, `send my-topic`) | The messaging operation type and the destination (`send my-exchange`), or just the operation type when the destination has no name (`send`) |
| `queue.process` | Integration-specific, sometimes containing per-message data (`my-queue process`, `order.created.12345 process`) | The messaging operation type and the destination (`process my-exchange`), or just the operation type when the destination has no name (`process`) |
| `queue.receive` | The kafkajs operation name (`poll my-topic`) | The messaging operation type and the destination (`receive my-topic`) |
| `cache.get`, `cache.put`, `cache.remove` | The cache key(s) (`user:123`), or for dataloader the operation and loader name (`dataloader.load usersLoader`) | The cache operation (`cache.get`, `cache.put`, `cache.remove`) |
| `db` (mongoose) | `mongoose.<Model>.<operation>` (`mongoose.BlogPost.findOne`) | The operation and the collection (`findOne blogposts`), the database namespace when there is no collection, or `mongodb` when the SDK has neither |

`navigation.redirect` spans are started through the same code path as navigation spans, so they get the same names.
Expand DownExpand Up@@ -959,6 +960,10 @@ A mongoose span's name is built from `db.collection.name`, so it holds the colle

Messaging span names now read `<operation type> <destination>` in every integration. The amqplib, kafkajs and NestJS BullMQ integrations used their own word order or verb, so their names change: `my-queue process` became `process my-queue`, amqplib's `publish` became `send`, and the kafkajs batch span's `poll` became `receive`. Cloudflare Queues and the kafkajs producer already matched the conventions, so their names are the same in both trace lifecycles. The operation name an integration reports upstream stays on `messaging.operation.name`.

Cache keys are unbounded, so they are no longer part of a cache span name. They remain available on the `cache.key` attribute, and every cache span now also carries a `cache.operation` attribute (`get`, `put`, `remove`) — the value the name is built from. That attribute is set in both trace lifecycles. This affects the redis/ioredis cache spans (`cachePrefixes`), the Nuxt and Nitro storage spans, and the dataloader spans.

A dataloader span no longer carries the loader's `name` either (`dataloader.load usersLoader` becomes `cache.get`), because the cache conventions have no slot for it in the name. It is reported on the `db.collection.name` attribute instead — a loader batches one entity type, so it is the closest thing dataloader has to a collection — and that attribute is set in both trace lifecycles. Unnamed loaders do not set it.

AWS SQS `SendMessage`, `SendMessageBatch` and `ReceiveMessage`, and SNS `Publish`, are messaging spans (e.g. `queue.publish`) rather than `rpc` ones now. Every other command on those clients, such as `DeleteMessage`, stays `rpc`. Their names follow the messaging conventions too, so the operation comes first (`my-queue receive` becomes `receive my-queue`, `my-topic send` becomes `send my-topic`). A streamed SNS `Publish` to a platform endpoint is named `send`, because the endpoint ARN it used to carry ends in a per-device id (`endpoint/GCM/myapp/<uuid> send`). The full ARN remains on `messaging.destination.name`.

An amqplib span's destination is the exchange it uses, or the routing key when it uses the default exchange. RabbitMQ binds every queue to the default exchange under a key equal to the queue's own name, so `sendToQueue` spans are named after their queue (`send my-queue`) instead of dropping the destination. `messaging.destination.name` reports the same value, and the routing key remains on `messaging.rabbitmq.destination.routing_key` in full.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,6 +47,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheMissSpan) {
expect(cacheMissSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: false },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -64,6 +65,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheHitSpan) {
expect(cacheHitSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -81,6 +83,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheSetSpan) {
expect(cacheSetSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: expect.stringMatching(/^(cache)?$/) },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,35 +38,38 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(setSpan).toBeDefined();
expect(setSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(setSpan?.name).toBe(prefixKey('alias:user'));
expect(setSpan?.name).toBe('cache.put');

// Test get (alias for getItem)
expect(findSpansByMethod('getItem').length).toBeGreaterThanOrEqual(1);
const getSpan = findByKey('getItem', prefixKey('alias:user'));
expect(getSpan).toBeDefined();
expect(getSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getSpan?.name).toBe(prefixKey('alias:user'));
expect(getSpan?.name).toBe('cache.get');

// Test has (alias for hasItem)
expect(findSpansByMethod('hasItem').length).toBeGreaterThanOrEqual(1);
const hasSpan = findByKey('hasItem', prefixKey('alias:user'));
expect(hasSpan).toBeDefined();
expect(hasSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -82,25 +85,27 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(delSpan).toBeDefined();
expect(delSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(delSpan?.name).toBe(prefixKey('alias:temp1'));
expect(delSpan?.name).toBe('cache.remove');

const removeSpan = findByKey('removeItem', prefixKey('alias:temp2'));
expect(removeSpan).toBeDefined();
expect(removeSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp2') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(removeSpan?.name).toBe(prefixKey('alias:temp2'));
expect(removeSpan?.name).toBe('cache.remove');

// Verify all spans have OK status
expect(allStorageSpans.length).toBeGreaterThan(0);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,14 +38,15 @@ test.describe('Storage Instrumentation', () => {
expect(setItemSpan).toBeDefined();
expect(setItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});

expect(setItemSpan?.name).toBe(prefixKey('user:123'));
expect(setItemSpan?.name).toBe('cache.put');

// Test setItemRaw spans
expect(findSpansByMethod('setItemRaw').length).toBeGreaterThanOrEqual(1);
Expand All@@ -54,6 +55,7 @@ test.describe('Storage Instrumentation', () => {
expect(setItemRawSpan).toBeDefined();
expect(setItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
'db.operation.name': { type: 'string', value: 'setItemRaw' },
Expand All@@ -67,6 +69,7 @@ test.describe('Storage Instrumentation', () => {
expect(hasItemSpan).toBeDefined();
expect(hasItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -81,21 +84,23 @@ test.describe('Storage Instrumentation', () => {
expect(getItemSpan).toBeDefined();
expect(getItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getItemSpan?.name).toBe(prefixKey('user:123'));
expect(getItemSpan?.name).toBe('cache.get');

// Test getItemRaw spans - should have cache hit attribute
expect(findSpansByMethod('getItemRaw').length).toBeGreaterThanOrEqual(1);
const getItemRawSpan = findSpanByCacheKey('getItemRaw', prefixKey('raw:data'));
expect(getItemRawSpan).toBeDefined();
expect(getItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -109,6 +114,7 @@ test.describe('Storage Instrumentation', () => {
expect(getKeysSpans.length).toBeGreaterThanOrEqual(1);
expect(getKeysSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'getKeys' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand All@@ -121,6 +127,7 @@ test.describe('Storage Instrumentation', () => {
expect(removeItemSpan).toBeDefined();
expect(removeItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('batch:1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
Expand All@@ -133,6 +140,7 @@ test.describe('Storage Instrumentation', () => {
expect(clearSpans.length).toBeGreaterThanOrEqual(1);
expect(clearSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'clear' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import type { SerializedStreamedSpanContainer } from '@sentry/core';
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

Expand DownExpand Up@@ -103,6 +104,7 @@ describe('dataloader auto-instrumentation', () => {
expect(namedLoadSpan?.op).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.origin).toBe(ORIGIN);
expect(namedLoadSpan?.status).toBe('ok');
expect(namedLoadSpan?.data?.['db.collection.name']).toBe('usersLoader');
},
})
.start();
Expand All@@ -113,5 +115,37 @@ describe('dataloader auto-instrumentation', () => {
await runner.makeRequest('get', '/named');
await runner.completed();
}, 30_000);

test('names spans after the cache operation when streamed', async () => {
const runner = createRunner()
.withEnv({ STREAMED: 'true' })
.expect({
span: (container: SerializedStreamedSpanContainer) => {
const namedLoadSpan = container.items.find(
span => span.attributes?.['db.operation.name']?.value === 'load',
);
expect(namedLoadSpan?.name).toBe('cache.get');
expect(namedLoadSpan?.attributes?.['sentry.op']?.value).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.attributes?.['cache.operation']?.value).toBe('get');
// The loader name is no longer part of the span name, it moved to `db.collection.name`.
expect(namedLoadSpan?.attributes?.['db.collection.name']?.value).toBe('usersLoader');
},
})
.expect({
span: (container: SerializedStreamedSpanContainer) => {
for (const [operation, op] of Object.entries(CACHE_MUTATION_OPS)) {
const span = container.items.find(item => item.attributes?.['db.operation.name']?.value === operation);
expect(span, `expected a ${operation} span`).toBeDefined();
expect(span?.name).toBe(op);
expect(span?.attributes?.['sentry.op']?.value).toBe(op);
}
},
})
.start();

await runner.makeRequest('get', '/named');
await runner.makeRequest('get', '/cache-ops');
await runner.completed();
}, 30_000);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions MIGRATION.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -923,6 +923,7 @@ The following span names were adjusted:
| `queue.publish` | Integration-specific (`publish my-exchange`, `send my-topic`) | The messaging operation type and the destination (`send my-exchange`), or just the operation type when the destination has no name (`send`) |
| `queue.process` | Integration-specific, sometimes containing per-message data (`my-queue process`, `order.created.12345 process`) | The messaging operation type and the destination (`process my-exchange`), or just the operation type when the destination has no name (`process`) |
| `queue.receive` | The kafkajs operation name (`poll my-topic`) | The messaging operation type and the destination (`receive my-topic`) |
| `cache.get`, `cache.put`, `cache.remove` | The cache key(s) (`user:123`), or for dataloader the operation and loader name (`dataloader.load usersLoader`) | The cache operation (`cache.get`, `cache.put`, `cache.remove`) |
| `db` (mongoose) | `mongoose.<Model>.<operation>` (`mongoose.BlogPost.findOne`) | The operation and the collection (`findOne blogposts`), the database namespace when there is no collection, or `mongodb` when the SDK has neither |

`navigation.redirect` spans are started through the same code path as navigation spans, so they get the same names.
Expand DownExpand Up@@ -959,6 +960,10 @@ A mongoose span's name is built from `db.collection.name`, so it holds the colle

Messaging span names now read `<operation type> <destination>` in every integration. The amqplib, kafkajs and NestJS BullMQ integrations used their own word order or verb, so their names change: `my-queue process` became `process my-queue`, amqplib's `publish` became `send`, and the kafkajs batch span's `poll` became `receive`. Cloudflare Queues and the kafkajs producer already matched the conventions, so their names are the same in both trace lifecycles. The operation name an integration reports upstream stays on `messaging.operation.name`.

Cache keys are unbounded, so they are no longer part of a cache span name. They remain available on the `cache.key` attribute, and every cache span now also carries a `cache.operation` attribute (`get`, `put`, `remove`) — the value the name is built from. That attribute is set in both trace lifecycles. This affects the redis/ioredis cache spans (`cachePrefixes`), the Nuxt and Nitro storage spans, and the dataloader spans.

A dataloader span no longer carries the loader's `name` either (`dataloader.load usersLoader` becomes `cache.get`), because the cache conventions have no slot for it in the name. It is reported on the `db.collection.name` attribute instead — a loader batches one entity type, so it is the closest thing dataloader has to a collection — and that attribute is set in both trace lifecycles. Unnamed loaders do not set it.

AWS SQS `SendMessage`, `SendMessageBatch` and `ReceiveMessage`, and SNS `Publish`, are messaging spans (e.g. `queue.publish`) rather than `rpc` ones now. Every other command on those clients, such as `DeleteMessage`, stays `rpc`. Their names follow the messaging conventions too, so the operation comes first (`my-queue receive` becomes `receive my-queue`, `my-topic send` becomes `send my-topic`). A streamed SNS `Publish` to a platform endpoint is named `send`, because the endpoint ARN it used to carry ends in a per-device id (`endpoint/GCM/myapp/<uuid> send`). The full ARN remains on `messaging.destination.name`.

An amqplib span's destination is the exchange it uses, or the routing key when it uses the default exchange. RabbitMQ binds every queue to the default exchange under a key equal to the queue's own name, so `sendToQueue` spans are named after their queue (`send my-queue`) instead of dropping the destination. `messaging.destination.name` reports the same value, and the routing key remains on `messaging.rabbitmq.destination.routing_key` in full.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,6 +47,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheMissSpan) {
expect(cacheMissSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: false },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -64,6 +65,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheHitSpan) {
expect(cacheHitSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
Expand All@@ -81,6 +83,7 @@ test.describe('Cache Instrumentation', () => {
if (cacheSetSpan) {
expect(cacheSetSpan.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: expect.stringMatching(/^(cache)?$/) },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,35 +38,38 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(setSpan).toBeDefined();
expect(setSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(setSpan?.name).toBe(prefixKey('alias:user'));
expect(setSpan?.name).toBe('cache.put');

// Test get (alias for getItem)
expect(findSpansByMethod('getItem').length).toBeGreaterThanOrEqual(1);
const getSpan = findByKey('getItem', prefixKey('alias:user'));
expect(getSpan).toBeDefined();
expect(getSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getSpan?.name).toBe(prefixKey('alias:user'));
expect(getSpan?.name).toBe('cache.get');

// Test has (alias for hasItem)
expect(findSpansByMethod('hasItem').length).toBeGreaterThanOrEqual(1);
const hasSpan = findByKey('hasItem', prefixKey('alias:user'));
expect(hasSpan).toBeDefined();
expect(hasSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:user') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -82,25 +85,27 @@ test.describe('Storage Instrumentation - Aliases', () => {
expect(delSpan).toBeDefined();
expect(delSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(delSpan?.name).toBe(prefixKey('alias:temp1'));
expect(delSpan?.name).toBe('cache.remove');

const removeSpan = findByKey('removeItem', prefixKey('alias:temp2'));
expect(removeSpan).toBeDefined();
expect(removeSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('alias:temp2') },
'db.operation.name': { type: 'string', value: 'removeItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(removeSpan?.name).toBe(prefixKey('alias:temp2'));
expect(removeSpan?.name).toBe('cache.remove');

// Verify all spans have OK status
expect(allStorageSpans.length).toBeGreaterThan(0);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,14 +38,15 @@ test.describe('Storage Instrumentation', () => {
expect(setItemSpan).toBeDefined();
expect(setItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
'db.operation.name': { type: 'string', value: 'setItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});

expect(setItemSpan?.name).toBe(prefixKey('user:123'));
expect(setItemSpan?.name).toBe('cache.put');

// Test setItemRaw spans
expect(findSpansByMethod('setItemRaw').length).toBeGreaterThanOrEqual(1);
Expand All@@ -54,6 +55,7 @@ test.describe('Storage Instrumentation', () => {
expect(setItemRawSpan).toBeDefined();
expect(setItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.put' },
'cache.operation': { type: 'string', value: 'put' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
'db.operation.name': { type: 'string', value: 'setItemRaw' },
Expand All@@ -67,6 +69,7 @@ test.describe('Storage Instrumentation', () => {
expect(hasItemSpan).toBeDefined();
expect(hasItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -81,21 +84,23 @@ test.describe('Storage Instrumentation', () => {
expect(getItemSpan).toBeDefined();
expect(getItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('user:123') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
'db.operation.name': { type: 'string', value: 'getItem' },
'db.collection.name': { type: 'string', value: 'test-storage' },
'db.system.name': { type: 'string', value: 'memory' },
});
expect(getItemSpan?.name).toBe(prefixKey('user:123'));
expect(getItemSpan?.name).toBe('cache.get');

// Test getItemRaw spans - should have cache hit attribute
expect(findSpansByMethod('getItemRaw').length).toBeGreaterThanOrEqual(1);
const getItemRawSpan = findSpanByCacheKey('getItemRaw', prefixKey('raw:data'));
expect(getItemRawSpan).toBeDefined();
expect(getItemRawSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('raw:data') },
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: { type: 'boolean', value: true },
Expand All@@ -109,6 +114,7 @@ test.describe('Storage Instrumentation', () => {
expect(getKeysSpans.length).toBeGreaterThanOrEqual(1);
expect(getKeysSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.get' },
'cache.operation': { type: 'string', value: 'get' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'getKeys' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand All@@ -121,6 +127,7 @@ test.describe('Storage Instrumentation', () => {
expect(removeItemSpan).toBeDefined();
expect(removeItemSpan?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: { type: 'string', value: prefixKey('batch:1') },
'db.operation.name': { type: 'string', value: 'removeItem' },
Expand All@@ -133,6 +140,7 @@ test.describe('Storage Instrumentation', () => {
expect(clearSpans.length).toBeGreaterThanOrEqual(1);
expect(clearSpans[0]?.attributes).toMatchObject({
'sentry.op': { type: 'string', value: 'cache.remove' },
'cache.operation': { type: 'string', value: 'remove' },
'sentry.origin': { type: 'string', value: 'auto.cache.nuxt' },
'db.operation.name': { type: 'string', value: 'clear' },
'db.collection.name': { type: 'string', value: 'test-storage' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import type { SerializedStreamedSpanContainer } from '@sentry/core';
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

Expand DownExpand Up@@ -103,6 +104,7 @@ describe('dataloader auto-instrumentation', () => {
expect(namedLoadSpan?.op).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.origin).toBe(ORIGIN);
expect(namedLoadSpan?.status).toBe('ok');
expect(namedLoadSpan?.data?.['db.collection.name']).toBe('usersLoader');
},
})
.start();
Expand All@@ -113,5 +115,37 @@ describe('dataloader auto-instrumentation', () => {
await runner.makeRequest('get', '/named');
await runner.completed();
}, 30_000);

test('names spans after the cache operation when streamed', async () => {
const runner = createRunner()
.withEnv({ STREAMED: 'true' })
.expect({
span: (container: SerializedStreamedSpanContainer) => {
const namedLoadSpan = container.items.find(
span => span.attributes?.['db.operation.name']?.value === 'load',
);
expect(namedLoadSpan?.name).toBe('cache.get');
expect(namedLoadSpan?.attributes?.['sentry.op']?.value).toBe(CACHE_GET_OP);
expect(namedLoadSpan?.attributes?.['cache.operation']?.value).toBe('get');
// The loader name is no longer part of the span name, it moved to `db.collection.name`.
expect(namedLoadSpan?.attributes?.['db.collection.name']?.value).toBe('usersLoader');
},
})
.expect({
span: (container: SerializedStreamedSpanContainer) => {
for (const [operation, op] of Object.entries(CACHE_MUTATION_OPS)) {
const span = container.items.find(item => item.attributes?.['db.operation.name']?.value === operation);
expect(span, `expected a ${operation} span`).toBeDefined();
expect(span?.name).toBe(op);
expect(span?.attributes?.['sentry.op']?.value).toBe(op);
}
},
})
.start();

await runner.makeRequest('get', '/named');
await runner.makeRequest('get', '/cache-ops');
await runner.completed();
}, 30_000);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',
traceLifecycle: process.env.STREAMED === 'true' ? 'stream' : 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
Expand Down
Loading
Loading