UNPKG

@sentry/nextjs

Version:
255 lines (252 loc) • 9.83 kB
import { createHash } from 'node:crypto'; import { TraceFlags } from '@opentelemetry/api'; import { CODE_FILE_PATH, CACHE_OPERATION, CACHE_KEY, SENTRY_ORIGIN, CACHE_HIT, CACHE_ITEM_AGE, CACHE_TTL, CACHE_TAGS, SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; import { CACHE_GET, CACHE_PUT } from '@sentry/conventions/op'; import { defineIntegration, hasSpansEnabled, debug, fill, spanToTraceHeader, getActiveSpan, spanIsSampled, getClient, startSpan, CACHE_OPERATION_NAMES, hasSpanStreamingEnabled, timestampInSeconds, extractTraceparentData, LRUMap } from '@sentry/core'; import { DEBUG_BUILD } from '../common/debug-build.js'; import { getCacheFunctionSourceFile } from './useCacheSourceFile.js'; const NEXT_CACHE_HANDLERS_MAP = /* @__PURE__ */ Symbol.for("@next/cache-handlers-map"); const NEXT_PRIVATE_CACHE_HANDLER = /* @__PURE__ */ Symbol.for("@next/cache-handlers-private"); const SENTRY_CACHE_INSTRUMENTED = /* @__PURE__ */ Symbol.for("sentry.nextjs.cacheHandlersInstrumented"); const SENTRY_WRAPPED_HANDLERS = /* @__PURE__ */ Symbol.for("sentry.nextjs.wrappedCacheHandlers"); const SENTRY_CACHE_ORIGINS = /* @__PURE__ */ Symbol.for("sentry.nextjs.cacheOrigins"); const INTEGRATION_NAME = "NextjsUseCache"; const CACHE_SPAN_ORIGIN = "auto.cache.nextjs"; const CACHE_ORIGIN_LINK_TYPE = "cache_origin"; const CACHE_ORIGINS_MAX_SIZE = 2e3; const NEXT_INFINITE_CACHE = 4294967294; const NEXT_DEV_MIN_EXPIRE = 300; function keyDigest(cacheKey) { return createHash("sha1").update(cacheKey).digest("hex").slice(0, 12); } function shouldRecordCacheSpan() { const activeSpan = getActiveSpan(); return !!activeSpan && spanIsSampled(activeSpan); } function startCacheSpan(op, digest, extraAttributes, callback) { const client = getClient(); return startSpan( { // low cardinality name for span streaming, so we can't fall back to the cache key name: client && hasSpanStreamingEnabled(client) ? op : digest, op, attributes: { [SENTRY_ORIGIN]: CACHE_SPAN_ORIGIN, [CACHE_KEY]: [digest], [CACHE_OPERATION]: CACHE_OPERATION_NAMES[op], ...extraAttributes } }, callback ); } function isExpired(ageMs, expire) { if (ageMs === void 0 || typeof expire !== "number") { return false; } const effectiveExpire = process.env.NODE_ENV === "development" ? Math.max(expire, NEXT_DEV_MIN_EXPIRE) : expire; return ageMs > effectiveExpire * 1e3; } function recordCacheEntry(span, entry) { if (entry === void 0) { span.setAttribute(CACHE_HIT, false); return false; } const { timestamp, expire, tags } = entry ?? {}; const ageMs = typeof timestamp === "number" ? timestampInSeconds() * 1e3 - timestamp : void 0; const hit = !isExpired(ageMs, expire); span.setAttribute(CACHE_HIT, hit); if (ageMs !== void 0) { span.setAttribute(CACHE_ITEM_AGE, Math.max(0, Math.round(ageMs / 1e3))); } if (typeof expire === "number" && expire >= 0 && expire < NEXT_INFINITE_CACHE) { span.setAttribute(CACHE_TTL, expire); } const stringTags = Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : []; if (stringTags.length > 0) { span.setAttribute(CACHE_TAGS, stringTags); } return hit; } function getCacheOrigins() { const globalWithCacheHandlers = globalThis; if (!globalWithCacheHandlers[SENTRY_CACHE_ORIGINS]) { globalWithCacheHandlers[SENTRY_CACHE_ORIGINS] = new LRUMap(CACHE_ORIGINS_MAX_SIZE); } return globalWithCacheHandlers[SENTRY_CACHE_ORIGINS]; } function linkCacheOrigin(span, originKey, entry) { const context = getPatchedCacheOrigin(entry) ?? getRememberedCacheOrigin(originKey, entry); if (context) { span.addLink({ context, attributes: { [SENTRY_LINK_TYPE]: CACHE_ORIGIN_LINK_TYPE } }); } } function getPatchedCacheOrigin(entry) { const { __sentryCacheOrigin } = entry ?? {}; const origin = typeof __sentryCacheOrigin === "string" ? extractTraceparentData(__sentryCacheOrigin) : void 0; return origin?.traceId && origin.parentSpanId && origin.parentSampled ? { traceId: origin.traceId, spanId: origin.parentSpanId, traceFlags: TraceFlags.SAMPLED } : void 0; } function getRememberedCacheOrigin(originKey, entry) { const origin = getCacheOrigins().get(originKey); const { timestamp } = entry ?? {}; return origin && origin.entryTimestamp === timestamp ? origin.context : void 0; } function patchCacheOrigin(pendingEntry, origin) { const patchedEntry = Promise.resolve(pendingEntry).then((entry) => { if (typeof entry === "object" && entry !== null) { try { entry.__sentryCacheOrigin = origin; } catch (error) { DEBUG_BUILD && debug.warn("Failed to patch a Next.js cache fill origin", error); } } return entry; }); patchedEntry.catch(() => void 0); return patchedEntry; } function rememberCacheOrigin(originKey, span, pendingEntry) { Promise.resolve(pendingEntry).then( (entry) => { try { const { timestamp } = entry ?? {}; if (typeof timestamp === "number") { getCacheOrigins().set(originKey, { context: span.spanContext(), entryTimestamp: timestamp }); } } catch (error) { DEBUG_BUILD && debug.warn("Failed to remember a Next.js cache fill origin", error); } }, () => { } ); } function isCacheHandler(value) { return typeof value === "object" && value !== null && typeof value.get === "function" && typeof value.set === "function"; } function getWrappedHandlers() { const globalWithCacheHandlers = globalThis; if (!globalWithCacheHandlers[SENTRY_WRAPPED_HANDLERS]) { globalWithCacheHandlers[SENTRY_WRAPPED_HANDLERS] = /* @__PURE__ */ new WeakSet(); } return globalWithCacheHandlers[SENTRY_WRAPPED_HANDLERS]; } let wrappedHandlerCount = 0; function instrumentHandler(handler) { try { const wrappedHandlers = getWrappedHandlers(); if (!isCacheHandler(handler) || wrappedHandlers.has(handler)) { return; } wrappedHandlers.add(handler); const originKeyPrefix = `${wrappedHandlerCount++}:`; fill(handler, "get", (originalGet) => { return function(cacheKey, softTags) { if (!shouldRecordCacheSpan()) { return originalGet.call(this, cacheKey, softTags); } const digest = keyDigest(cacheKey); return startCacheSpan( CACHE_GET, digest, {}, (span) => ( // `Promise.resolve` because custom handlers may return the entry synchronously. Promise.resolve(originalGet.call(this, cacheKey, softTags)).then((entry) => { try { if (recordCacheEntry(span, entry)) { linkCacheOrigin(span, originKeyPrefix + digest, entry); } } catch (error) { DEBUG_BUILD && debug.warn("Failed to read Next.js cache entry metadata", error); } return entry; }) ) ); }; }); fill(handler, "set", (originalSet) => { return function(cacheKey, pendingEntry) { if (!shouldRecordCacheSpan()) { return originalSet.call(this, cacheKey, pendingEntry); } const digest = keyDigest(cacheKey); const sourceFile = getCacheFunctionSourceFile(cacheKey); return startCacheSpan(CACHE_PUT, digest, sourceFile ? { [CODE_FILE_PATH]: sourceFile } : {}, (span) => { if (!span.isRecording()) { return originalSet.call(this, cacheKey, pendingEntry); } const patchedEntry = patchCacheOrigin(pendingEntry, spanToTraceHeader(span)); return Promise.resolve(originalSet.call(this, cacheKey, patchedEntry)).then((result) => { rememberCacheOrigin(originKeyPrefix + digest, span, patchedEntry); return result; }); }); }; }); } catch (error) { DEBUG_BUILD && debug.warn("Failed to instrument a Next.js cache handler", error); } } function instrumentHandlersMap(handlersMap) { for (const handler of handlersMap.values()) { instrumentHandler(handler); } fill(handlersMap, "set", (originalSet) => { return function(kind, handler) { const result = originalSet.call(this, kind, handler); instrumentHandler(handler); return result; }; }); } function instrumentWhenAssigned(symbol, onValue) { const globalWithCacheHandlers = globalThis; const existingValue = globalWithCacheHandlers[symbol]; if (existingValue !== void 0) { onValue(existingValue); return; } let storedValue; Object.defineProperty(globalWithCacheHandlers, symbol, { configurable: true, // Non-enumerable so the accessor stays out of copies/spreads of `globalThis` in apps that // never assign the registry. enumerable: false, get: () => storedValue, set: (value) => { storedValue = value; onValue(value); } }); } function _instrumentUseCacheHandlers() { try { const globalWithCacheHandlers = globalThis; if (globalWithCacheHandlers[SENTRY_CACHE_INSTRUMENTED]) { return; } globalWithCacheHandlers[SENTRY_CACHE_INSTRUMENTED] = true; instrumentWhenAssigned(NEXT_CACHE_HANDLERS_MAP, (value) => { if (value instanceof Map) { instrumentHandlersMap(value); } }); instrumentWhenAssigned(NEXT_PRIVATE_CACHE_HANDLER, instrumentHandler); } catch (error) { DEBUG_BUILD && debug.warn("Failed to instrument Next.js cache handlers", error); } } const nextjsUseCacheIntegration = defineIntegration(() => { return { name: INTEGRATION_NAME, setup(client) { if (hasSpansEnabled(client.getOptions())) { _instrumentUseCacheHandlers(); } } }; }); export { _instrumentUseCacheHandlers, nextjsUseCacheIntegration }; //# sourceMappingURL=useCacheInstrumentation.js.map