Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ test('Sends an error event to Sentry', async ({ request }) => {
expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual(
expect.objectContaining({
handled: false,
type: 'auto.http.nitro.onTraceError',
type: 'auto.diagnostic_channels.bind_span',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is no good, this should be somewhat similar to what it was before 😬

}),
);
});
Expand Down
47 changes: 5 additions & 42 deletions packages/deno/src/integrations/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@
// lacking `tracingChannel` (added in Deno 1.44.3).
// On older runtimes the integration becomes a no-op.
import * as dc from 'node:diagnostics_channel';
import type {
RedisDiagnosticChannelResponseHook,
RedisTracingChannel,
RedisTracingChannelFactory,
RedisTracingChannelSubscribers,
} from '@sentry/server-utils';
import type { RedisDiagnosticChannelResponseHook, RedisTracingChannelFactory } from '@sentry/server-utils';
import { subscribeRedisDiagnosticChannels } from '@sentry/server-utils';
import type { Integration, IntegrationFn, Span } from '@sentry/core';
import type { Integration, IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { setAsyncLocalStorageAsyncContextStrategy } from '../async';

Expand All @@ -23,49 +18,17 @@ export interface DenoRedisIntegrationOptions {
responseHook?: RedisDiagnosticChannelResponseHook;
}

/**
* Portable tracing-channel factory: wraps `node:diagnostics_channel.tracingChannel`
* and stamps `data._sentrySpan` from `transformStart` in the `start` subscriber.
*
* Unlike `@sentry/opentelemetry/tracing-channel`, this does not call `bindStore`
*/
type DataWithSpan<T> = T & { _sentrySpan?: Span };
type SubscriberFn<T> = (data: DataWithSpan<T>) => void;

const portableTracingChannel: RedisTracingChannelFactory = <T extends object>(
name: string,
transformStart: (data: T) => Span,
): RedisTracingChannel<T> => {
const channel = dc.tracingChannel<DataWithSpan<T>>(name);
return {
subscribe(subs: Partial<RedisTracingChannelSubscribers<T>>): void {
const userStart = subs.start as SubscriberFn<T> | undefined;
const composed: Record<string, SubscriberFn<T>> = {
start(data) {
data._sentrySpan = transformStart(data);
userStart?.(data);
},
};
for (const event of ['asyncStart', 'asyncEnd', 'end', 'error'] as const) {
const fn = subs[event] as SubscriberFn<T> | undefined;
if (fn) composed[event] = fn;
}
// Native subscribe is typed for the full subscriber set, but only the
// handlers actually present are invoked at runtime.
channel.subscribe(composed as unknown as Parameters<typeof channel.subscribe>[0]);
},
};
};

const _denoRedisIntegration = ((options: DenoRedisIntegrationOptions = {}) => {
return {
name: INTEGRATION_NAME,
setupOnce() {
if (!dc.tracingChannel) {
return;
}
// The span is bound into Deno's AsyncLocalStorage context via the async-context
// strategy's `getTracingChannelBinding`, so the native channel can be passed directly.
setAsyncLocalStorageAsyncContextStrategy();
subscribeRedisDiagnosticChannels(portableTracingChannel, options.responseHook);
subscribeRedisDiagnosticChannels(dc.tracingChannel as RedisTracingChannelFactory, options.responseHook);
},
};
}) satisfies IntegrationFn;
Expand Down
2 changes: 1 addition & 1 deletion packages/nitro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@sentry/bundler-plugin-core": "^5.3.0",
"@sentry/core": "10.58.0",
"@sentry/node": "10.58.0",
"@sentry/opentelemetry": "10.58.0"
"@sentry/server-utils": "10.58.0"
},
"devDependencies": {
"nitro": "^3.0.260415-beta",
Expand Down
58 changes: 24 additions & 34 deletions packages/nitro/src/runtime/hooks/captureStorageEvents.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import {
captureException,
flushIfServerless,
GLOBAL_OBJ,
SEMANTIC_ATTRIBUTE_CACHE_HIT,
SEMANTIC_ATTRIBUTE_CACHE_KEY,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SPAN_STATUS_ERROR,
SPAN_STATUS_OK,
startSpanManual,
startInactiveSpan,
} from '@sentry/core';
import { tracingChannel, type TracingChannelContextWithSpan } from '@sentry/opentelemetry/tracing-channel';
import { bindTracingChannelToSpan } from '@sentry/server-utils';
import type { TraceContext } from 'unstorage/tracing';

const ORIGIN = 'auto.cache.nitro';
Expand Down Expand Up @@ -57,11 +56,12 @@ function setupStorageTracingChannel(operation: TracedOperation): void {
const keys = (data: TraceContext): string[] => data.keys ?? [];
const mountBase = (data: TraceContext): string => (data.base ?? '').replace(/:$/, '');

const channel = tracingChannel<TraceContext>(`unstorage.${operation}`, data => {
const cacheKeys = keys(data);
bindTracingChannelToSpan(
tracingChannel<TraceContext>(`unstorage.${operation}`),
data => {
const cacheKeys = keys(data);

return startSpanManual(
{
return startInactiveSpan({
name: cacheKeys.join(', ') || operation,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `cache.${normalizeMethodName(operation)}`,
Expand All @@ -71,34 +71,24 @@ function setupStorageTracingChannel(operation: TracedOperation): void {
'db.collection.name': mountBase(data),
'db.system.name': data.driver?.name ?? 'unknown',
},
},
span => span,
);
});

channel.subscribe({
asyncEnd(data: TracingChannelContextWithSpan<TraceContext & { result?: unknown }>) {
if (data._sentrySpan && CACHE_HIT_OPERATIONS.has(operation)) {
const hit = operation === 'hasItem' ? Boolean(data.result) : isCacheHit(data.keys?.[0], data.result);
data._sentrySpan.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, hit);
}

data._sentrySpan?.setStatus({ code: SPAN_STATUS_OK });
data._sentrySpan?.end();

void flushIfServerless();
},
error(data: TracingChannelContextWithSpan<TraceContext & { error?: unknown }>) {
captureException(data.error, {
mechanism: { handled: false, type: ORIGIN },
});

data._sentrySpan?.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
data._sentrySpan?.end();

void flushIfServerless();
},
});
{
beforeSpanEnd(span, data) {
// Auto-capture + error status is handled by the binding; only enrich the success path.
if (!('error' in data)) {
const result = (data as { result?: unknown }).result;
if (CACHE_HIT_OPERATIONS.has(operation)) {
const hit = operation === 'hasItem' ? Boolean(result) : isCacheHit(data.keys?.[0], result);
span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, hit);
}
span.setStatus({ code: SPAN_STATUS_OK });
}

void flushIfServerless();
},
},
);
}

function normalizeMethodName(methodName: string): string {
Expand Down
Loading
Loading