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
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.60.0",
"@sentry/node": "10.60.0",
"@sentry/opentelemetry": "10.60.0"
"@sentry/server-utils": "10.60.0"
},
"devDependencies": {
"nitro": "^3.0.260415-beta",
Expand Down
59 changes: 24 additions & 35 deletions packages/nitro/src/runtime/hooks/captureStorageEvents.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
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 +55,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 +70,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) {
// Error status is set by the binding; the error itself is captured at the request boundary,
// not here (cache ops aren't an error boundary). Only enrich the success path.
if (!('error' in data)) {
const result = (data as { result?: unknown }).result;
Comment on lines 70 to +80

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The refactoring to bindTracingChannelToSpan for storage events unintentionally changes the error mechanism type, losing the specific 'auto.cache.nitro' context.
Severity: LOW

Suggested Fix

Preserve the original error mechanism type for storage events by providing a custom captureError function when calling bindTracingChannelToSpan, similar to how HTTP events are handled. This function should return a mechanism object with the type set to 'auto.cache.nitro'. For example: captureError: () => ({ mechanism: { handled: false, type: 'auto.cache.nitro' } }).

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/nitro/src/runtime/hooks/captureStorageEvents.ts#L71-L80

Potential issue: During the migration to `bindTracingChannelToSpan`, the error capturing
for storage events was changed to use the default mechanism. This results in errors
being reported with a generic mechanism type of `'auto.diagnostic_channels.bind_span'`
instead of the previous, more specific `'auto.cache.nitro'`. This is inconsistent with
the handling of HTTP events in the same file, which explicitly preserve their custom
mechanism type. This change can negatively affect error grouping and filtering in
Sentry, making it harder to track and analyze storage-related failures.

Did we get this right? 👍 / 👎 to inform future reviews.

if (CACHE_HIT_OPERATIONS.has(operation)) {
const hit = operation === 'hasItem' ? Boolean(result) : isCacheHit(data.keys?.[0], result);
span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, hit);
}
}

void flushIfServerless();
},
},
);
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
}

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