From a0eddae2c49a3fa30fbb698d986c3f17567de0e4 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 2 Oct 2025 23:31:07 +0300 Subject: [PATCH 01/13] feat(nuxt): instrument storage drivers --- packages/nuxt/src/module.ts | 1 + .../src/runtime/plugins/storage.server.ts | 145 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 packages/nuxt/src/runtime/plugins/storage.server.ts diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 1e806e4dc2eb..d4ea168357e5 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -90,6 +90,7 @@ export default defineNuxtModule({ if (serverConfigFile) { addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server')); + addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/storage.server')); addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/route-detector.server'), diff --git a/packages/nuxt/src/runtime/plugins/storage.server.ts b/packages/nuxt/src/runtime/plugins/storage.server.ts new file mode 100644 index 000000000000..1acddcece8ef --- /dev/null +++ b/packages/nuxt/src/runtime/plugins/storage.server.ts @@ -0,0 +1,145 @@ +import { + type SpanAttributes, + captureException, + debug, + flushIfServerless, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SPAN_STATUS_ERROR, + SPAN_STATUS_OK, + startSpan, +} from '@sentry/core'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { defineNitroPlugin, useStorage } from 'nitropack/runtime'; +import type { Driver } from 'unstorage'; + +/** + * Creates a Nitro plugin that instruments the storage driver. + */ +export default defineNitroPlugin(async _nitroApp => { + // This runs at runtime when the Nitro server starts + const storage = useStorage(); + + // exclude mounts that are not relevant for instrumentation for a few reasons: + // Nitro mounts some development-only mount points that are not relevant for instrumentation + // https://nitro.build/guide/storage#development-only-mount-points + const excludeMounts = new Set(['build:', 'cache:', 'root:', 'data:', 'src:', 'assets:']); + + debug.log('[Storage Instrumentation] Starting to instrument storage drivers...'); + + // Get all mounted storage drivers + const mounts = storage.getMounts(); + for (const mount of mounts) { + // Skip excluded mounts and root mount + if (!mount.base || excludeMounts.has(mount.base)) { + continue; + } + + debug.log(`[Storage Instrumentation] Instrumenting mount: "${mount.base}"`); + + const driver = instrumentDriver(mount.driver, mount.base); + + try { + // Remount with instrumented driver + await storage.unmount(mount.base); + await storage.mount(mount.base, driver); + } catch { + debug.error(`[Storage Instrumentation] Failed to unmount mount: "${mount.base}"`); + } + } +}); + +/** + * Instruments a driver by wrapping all method calls using proxies. + */ +function instrumentDriver(driver: Driver, mountBase: string): Driver { + // List of driver methods to instrument + const methodsToInstrument: (keyof Driver)[] = [ + 'hasItem', + 'getItem', + 'getItemRaw', + 'getItems', + 'setItem', + 'setItemRaw', + 'setItems', + 'removeItem', + 'getKeys', + 'getMeta', + 'clear', + 'dispose', + ]; + + for (const methodName of methodsToInstrument) { + const original = driver[methodName]; + // Skip if method doesn't exist on this driver + if (typeof original !== 'function') { + continue; + } + + // Replace with instrumented + driver[methodName] = createMethodWrapper(original, methodName, driver.name ?? 'unknown', mountBase); + } + + return driver; +} + +/** + * Creates an instrumented method for the given method. + */ +function createMethodWrapper( + original: (...args: unknown[]) => unknown, + methodName: string, + driverName: string, + mountBase: string, +): (...args: unknown[]) => unknown { + return new Proxy(original, { + async apply(target, thisArg, args) { + const attributes = getSpanAttributes(methodName, driverName ?? 'unknown', mountBase); + + return startSpan( + { + name: `storage.${methodName}`, + attributes, + }, + async span => { + try { + const result = await target.apply(thisArg, args); + span.setStatus({ code: SPAN_STATUS_OK }); + + return result; + } catch (error) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + captureException(error, { + mechanism: { + handled: false, + type: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN], + }, + }); + + // Re-throw the error to be handled by the caller + throw error; + } finally { + await flushIfServerless(); + } + }, + ); + }, + }); +} + +/** + * Gets the span attributes for the storage method. + */ +function getSpanAttributes(methodName: string, driverName: string, mountBase: string): SpanAttributes { + const attributes: SpanAttributes = { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'app.storage.nuxt', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.app.storage.nuxt', + 'nuxt.storage.op': methodName, + 'nuxt.storage.driver': driverName, + 'nuxt.storage.mount': mountBase, + }; + + return attributes; +} From 830c6b1a338742769d5cb1192f99216a812d2124 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 3 Oct 2025 02:05:39 +0300 Subject: [PATCH 02/13] refactor: only instrument user-defined storage --- packages/nuxt/src/common/server-template.ts | 17 +++++++++++++++ packages/nuxt/src/module.ts | 3 ++- .../src/runtime/plugins/storage.server.ts | 11 +++++----- packages/nuxt/src/vite/storageConfig.ts | 21 +++++++++++++++++++ 4 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 packages/nuxt/src/common/server-template.ts create mode 100644 packages/nuxt/src/vite/storageConfig.ts diff --git a/packages/nuxt/src/common/server-template.ts b/packages/nuxt/src/common/server-template.ts new file mode 100644 index 000000000000..7f0fafe6d8cb --- /dev/null +++ b/packages/nuxt/src/common/server-template.ts @@ -0,0 +1,17 @@ +import { useNuxt } from '@nuxt/kit'; +import type { NuxtTemplate } from 'nuxt/schema'; + +/** + * Adds a virtual file that can be used within the Nuxt Nitro server build. + * Available in NuxtKit v4, so we are porting it here. + * https://github.com/nuxt/nuxt/blob/d6df732eec1a3bd442bdb325b0335beb7e10cd64/packages/kit/src/template.ts#L55-L62 + */ +export function addServerTemplate(template: NuxtTemplate): NuxtTemplate { + const nuxt = useNuxt(); + if (template.filename) { + nuxt.options.nitro.virtual ||= {}; + nuxt.options.nitro.virtual[template.filename] = template.getContents; + } + + return template; +} diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index d4ea168357e5..947eb2710f4d 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -12,6 +12,7 @@ import type { SentryNuxtModuleOptions } from './common/types'; import { addDynamicImportEntryFileWrapper, addSentryTopImport, addServerConfigToBuild } from './vite/addServerConfig'; import { addMiddlewareImports, addMiddlewareInstrumentation } from './vite/middlewareConfig'; import { setupSourceMaps } from './vite/sourceMaps'; +import { addStorageInstrumentation } from './vite/storageConfig'; import { addOTelCommonJSImportAlias, findDefaultSdkInitFile } from './vite/utils'; export type ModuleOptions = SentryNuxtModuleOptions; @@ -90,7 +91,6 @@ export default defineNuxtModule({ if (serverConfigFile) { addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server')); - addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/storage.server')); addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/route-detector.server'), @@ -127,6 +127,7 @@ export default defineNuxtModule({ // Preps the the middleware instrumentation module. if (serverConfigFile) { addMiddlewareImports(); + addStorageInstrumentation(nuxt); } nuxt.hooks.hook('nitro:init', nitro => { diff --git a/packages/nuxt/src/runtime/plugins/storage.server.ts b/packages/nuxt/src/runtime/plugins/storage.server.ts index 1acddcece8ef..5127ff2253fd 100644 --- a/packages/nuxt/src/runtime/plugins/storage.server.ts +++ b/packages/nuxt/src/runtime/plugins/storage.server.ts @@ -13,6 +13,8 @@ import { // eslint-disable-next-line import/no-extraneous-dependencies import { defineNitroPlugin, useStorage } from 'nitropack/runtime'; import type { Driver } from 'unstorage'; +// @ts-expect-error - This is a virtual module +import { userStorageMounts } from '#sentry/storage-config.mjs'; /** * Creates a Nitro plugin that instruments the storage driver. @@ -20,11 +22,8 @@ import type { Driver } from 'unstorage'; export default defineNitroPlugin(async _nitroApp => { // This runs at runtime when the Nitro server starts const storage = useStorage(); - - // exclude mounts that are not relevant for instrumentation for a few reasons: - // Nitro mounts some development-only mount points that are not relevant for instrumentation - // https://nitro.build/guide/storage#development-only-mount-points - const excludeMounts = new Set(['build:', 'cache:', 'root:', 'data:', 'src:', 'assets:']); + // Mounts are suffixed with a colon, so we need to add it to the set items + const userMounts = new Set((userStorageMounts as string[]).map(m => `${m}:`)); debug.log('[Storage Instrumentation] Starting to instrument storage drivers...'); @@ -32,7 +31,7 @@ export default defineNitroPlugin(async _nitroApp => { const mounts = storage.getMounts(); for (const mount of mounts) { // Skip excluded mounts and root mount - if (!mount.base || excludeMounts.has(mount.base)) { + if (!userMounts.has(mount.base)) { continue; } diff --git a/packages/nuxt/src/vite/storageConfig.ts b/packages/nuxt/src/vite/storageConfig.ts new file mode 100644 index 000000000000..2d8e1fe457ef --- /dev/null +++ b/packages/nuxt/src/vite/storageConfig.ts @@ -0,0 +1,21 @@ +import { addServerPlugin, createResolver } from '@nuxt/kit'; +import type { Nuxt } from 'nuxt/schema'; +import { addServerTemplate } from '../common/server-template'; + +/** + * Prepares the storage config export to be used in the runtime storage instrumentation. + */ +export function addStorageInstrumentation(nuxt: Nuxt): void { + const moduleDirResolver = createResolver(import.meta.url); + const userStorageMounts = Object.keys(nuxt.options.nitro.storage || {}); + + // Create a virtual module to pass this data to runtime + addServerTemplate({ + filename: '#sentry/storage-config.mjs', + getContents: () => { + return `export const userStorageMounts = ${JSON.stringify(userStorageMounts)};`; + }, + }); + + addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/storage.server')); +} From fa0d1bcb6827aace181d5a1c1c5fd5c0c511af58 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 3 Oct 2025 14:35:49 +0300 Subject: [PATCH 03/13] feat: added more logs and instrument drivers in-place --- .../src/runtime/plugins/storage.server.ts | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/storage.server.ts b/packages/nuxt/src/runtime/plugins/storage.server.ts index 5127ff2253fd..fb0c2478df9e 100644 --- a/packages/nuxt/src/runtime/plugins/storage.server.ts +++ b/packages/nuxt/src/runtime/plugins/storage.server.ts @@ -12,10 +12,14 @@ import { } from '@sentry/core'; // eslint-disable-next-line import/no-extraneous-dependencies import { defineNitroPlugin, useStorage } from 'nitropack/runtime'; -import type { Driver } from 'unstorage'; +import type { Driver, Storage } from 'unstorage'; // @ts-expect-error - This is a virtual module import { userStorageMounts } from '#sentry/storage-config.mjs'; +type MaybeInstrumentedDriver = Driver & { + __sentry_instrumented__?: boolean; +}; + /** * Creates a Nitro plugin that instruments the storage driver. */ @@ -35,24 +39,30 @@ export default defineNitroPlugin(async _nitroApp => { continue; } - debug.log(`[Storage Instrumentation] Instrumenting mount: "${mount.base}"`); - - const driver = instrumentDriver(mount.driver, mount.base); - try { - // Remount with instrumented driver - await storage.unmount(mount.base); - await storage.mount(mount.base, driver); + instrumentDriver(mount.driver, mount.base); } catch { debug.error(`[Storage Instrumentation] Failed to unmount mount: "${mount.base}"`); } + + // Wrap the mount method to instrument future mounts + storage.mount = wrapStorageMount(storage); } }); /** * Instruments a driver by wrapping all method calls using proxies. */ -function instrumentDriver(driver: Driver, mountBase: string): Driver { +function instrumentDriver(driver: MaybeInstrumentedDriver, mountBase: string): Driver { + // Already instrumented, skip... + if (driver.__sentry_instrumented__) { + debug.log(`[Storage Instrumentation] Driver already instrumented: "${driver.name}". Skipping...`); + + return driver; + } + + debug.log(`[Storage Instrumentation] Instrumenting driver: "${driver.name}" on mount: "${mountBase}"`); + // List of driver methods to instrument const methodsToInstrument: (keyof Driver)[] = [ 'hasItem', @@ -80,6 +90,9 @@ function instrumentDriver(driver: Driver, mountBase: string): Driver { driver[methodName] = createMethodWrapper(original, methodName, driver.name ?? 'unknown', mountBase); } + // Mark as instrumented + driver.__sentry_instrumented__ = true; + return driver; } @@ -96,6 +109,8 @@ function createMethodWrapper( async apply(target, thisArg, args) { const attributes = getSpanAttributes(methodName, driverName ?? 'unknown', mountBase); + debug.log(`[Storage Instrumentation] Running method: "${methodName}" on driver: "${driverName}"`); + return startSpan( { name: `storage.${methodName}`, @@ -127,6 +142,23 @@ function createMethodWrapper( }); } +/** + * Wraps the storage mount method to instrument the driver. + */ +function wrapStorageMount(storage: Storage): Storage['mount'] { + const original = storage.mount; + + function mountWithInstrumentation(base: string, driver: Driver): Storage { + debug.log(`[Storage Instrumentation] Instrumenting mount: "${base}"`); + + const instrumentedDriver = instrumentDriver(driver, base); + + return original(base, instrumentedDriver); + } + + return mountWithInstrumentation; +} + /** * Gets the span attributes for the storage method. */ From e1f8395d1f3fa0f90c3286b6f4ac56fc4a51ae03 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 3 Oct 2025 15:43:46 +0300 Subject: [PATCH 04/13] refactor: shorter logging messages --- packages/nuxt/src/runtime/plugins/storage.server.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/storage.server.ts b/packages/nuxt/src/runtime/plugins/storage.server.ts index fb0c2478df9e..b38f672bf088 100644 --- a/packages/nuxt/src/runtime/plugins/storage.server.ts +++ b/packages/nuxt/src/runtime/plugins/storage.server.ts @@ -29,7 +29,7 @@ export default defineNitroPlugin(async _nitroApp => { // Mounts are suffixed with a colon, so we need to add it to the set items const userMounts = new Set((userStorageMounts as string[]).map(m => `${m}:`)); - debug.log('[Storage Instrumentation] Starting to instrument storage drivers...'); + debug.log('[storage] Starting to instrument storage drivers...'); // Get all mounted storage drivers const mounts = storage.getMounts(); @@ -42,7 +42,7 @@ export default defineNitroPlugin(async _nitroApp => { try { instrumentDriver(mount.driver, mount.base); } catch { - debug.error(`[Storage Instrumentation] Failed to unmount mount: "${mount.base}"`); + debug.error(`[storage] Failed to unmount mount: "${mount.base}"`); } // Wrap the mount method to instrument future mounts @@ -56,12 +56,12 @@ export default defineNitroPlugin(async _nitroApp => { function instrumentDriver(driver: MaybeInstrumentedDriver, mountBase: string): Driver { // Already instrumented, skip... if (driver.__sentry_instrumented__) { - debug.log(`[Storage Instrumentation] Driver already instrumented: "${driver.name}". Skipping...`); + debug.log(`[storage] Driver already instrumented: "${driver.name}". Skipping...`); return driver; } - debug.log(`[Storage Instrumentation] Instrumenting driver: "${driver.name}" on mount: "${mountBase}"`); + debug.log(`[storage] Instrumenting driver: "${driver.name}" on mount: "${mountBase}"`); // List of driver methods to instrument const methodsToInstrument: (keyof Driver)[] = [ @@ -109,7 +109,7 @@ function createMethodWrapper( async apply(target, thisArg, args) { const attributes = getSpanAttributes(methodName, driverName ?? 'unknown', mountBase); - debug.log(`[Storage Instrumentation] Running method: "${methodName}" on driver: "${driverName}"`); + debug.log(`[storage] Running method: "${methodName}" on driver: "${driverName}"`); return startSpan( { @@ -149,7 +149,7 @@ function wrapStorageMount(storage: Storage): Storage['mount'] { const original = storage.mount; function mountWithInstrumentation(base: string, driver: Driver): Storage { - debug.log(`[Storage Instrumentation] Instrumenting mount: "${base}"`); + debug.log(`[storage] Instrumenting mount: "${base}"`); const instrumentedDriver = instrumentDriver(driver, base); From aa9acfc6d91b1f9a2081d0348618132c06bb9aca Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 3 Oct 2025 16:16:50 +0300 Subject: [PATCH 05/13] feat: remove disposed from instrumentation list --- packages/nuxt/src/runtime/plugins/storage.server.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nuxt/src/runtime/plugins/storage.server.ts b/packages/nuxt/src/runtime/plugins/storage.server.ts index b38f672bf088..dd722e0bdda3 100644 --- a/packages/nuxt/src/runtime/plugins/storage.server.ts +++ b/packages/nuxt/src/runtime/plugins/storage.server.ts @@ -76,7 +76,6 @@ function instrumentDriver(driver: MaybeInstrumentedDriver, mountBase: string): D 'getKeys', 'getMeta', 'clear', - 'dispose', ]; for (const methodName of methodsToInstrument) { From bc7be790d197ba6b99ba23a98fd3e19b87c0d4d5 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 3 Oct 2025 17:06:57 +0300 Subject: [PATCH 06/13] feat: clean up attributes and use semantic cache ops --- .../src/runtime/plugins/storage.server.ts | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/storage.server.ts b/packages/nuxt/src/runtime/plugins/storage.server.ts index dd722e0bdda3..6ef2109df2c2 100644 --- a/packages/nuxt/src/runtime/plugins/storage.server.ts +++ b/packages/nuxt/src/runtime/plugins/storage.server.ts @@ -3,9 +3,10 @@ import { captureException, debug, flushIfServerless, + SEMANTIC_ATTRIBUTE_CACHE_HIT, + SEMANTIC_ATTRIBUTE_CACHE_KEY, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SPAN_STATUS_ERROR, SPAN_STATUS_OK, startSpan, @@ -20,6 +21,11 @@ type MaybeInstrumentedDriver = Driver & { __sentry_instrumented__?: boolean; }; +/** + * Methods that should have a attribute to indicate a cache hit. + */ +const KEYED_METHODS = new Set(['hasItem', 'getItem', 'getItemRaw', 'getItems']); + /** * Creates a Nitro plugin that instruments the storage driver. */ @@ -74,7 +80,6 @@ function instrumentDriver(driver: MaybeInstrumentedDriver, mountBase: string): D 'setItems', 'removeItem', 'getKeys', - 'getMeta', 'clear', ]; @@ -86,7 +91,7 @@ function instrumentDriver(driver: MaybeInstrumentedDriver, mountBase: string): D } // Replace with instrumented - driver[methodName] = createMethodWrapper(original, methodName, driver.name ?? 'unknown', mountBase); + driver[methodName] = createMethodWrapper(original, methodName, driver, mountBase); } // Mark as instrumented @@ -101,18 +106,20 @@ function instrumentDriver(driver: MaybeInstrumentedDriver, mountBase: string): D function createMethodWrapper( original: (...args: unknown[]) => unknown, methodName: string, - driverName: string, + driver: Driver, mountBase: string, ): (...args: unknown[]) => unknown { return new Proxy(original, { async apply(target, thisArg, args) { - const attributes = getSpanAttributes(methodName, driverName ?? 'unknown', mountBase); + const attributes = getSpanAttributes(methodName, driver, mountBase, args); - debug.log(`[storage] Running method: "${methodName}" on driver: "${driverName}"`); + debug.log(`[storage] Running method: "${methodName}" on driver: "${driver.name ?? 'unknown'}"`); + + const spanName = KEYED_METHODS.has(methodName) ? String(args?.[0]) : `storage.${normalizeMethodName(methodName)}`; return startSpan( { - name: `storage.${methodName}`, + name: spanName, attributes, }, async span => { @@ -120,6 +127,10 @@ function createMethodWrapper( const result = await target.apply(thisArg, args); span.setStatus({ code: SPAN_STATUS_OK }); + if (KEYED_METHODS.has(methodName)) { + span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, true); + } + return result; } catch (error) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); @@ -161,15 +172,26 @@ function wrapStorageMount(storage: Storage): Storage['mount'] { /** * Gets the span attributes for the storage method. */ -function getSpanAttributes(methodName: string, driverName: string, mountBase: string): SpanAttributes { +function getSpanAttributes(methodName: string, driver: Driver, mountBase: string, args: unknown[]): SpanAttributes { const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'app.storage.nuxt', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.app.storage.nuxt', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `cache.${normalizeMethodName(methodName)}`, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', 'nuxt.storage.op': methodName, - 'nuxt.storage.driver': driverName, 'nuxt.storage.mount': mountBase, + 'nuxt.storage.driver': driver.name ?? 'unknown', }; + // Add the key if it's a get/set/del call + if (args?.[0] && typeof args[0] === 'string') { + attributes[SEMANTIC_ATTRIBUTE_CACHE_KEY] = `${mountBase}${args[0]}`; + } + return attributes; } + +/** + * Normalizes the method name to snake_case to be used in span names or op. + */ +function normalizeMethodName(methodName: string): string { + return methodName.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); +} From ef9cc6af7feebd88dc5042e7d8d0eaffcfbcbfe0 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 3 Oct 2025 18:13:05 +0300 Subject: [PATCH 07/13] fix: show global key path on span name and adjust operation attrs --- .../src/runtime/plugins/storage.server.ts | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/storage.server.ts b/packages/nuxt/src/runtime/plugins/storage.server.ts index 6ef2109df2c2..d966f9077ef9 100644 --- a/packages/nuxt/src/runtime/plugins/storage.server.ts +++ b/packages/nuxt/src/runtime/plugins/storage.server.ts @@ -21,10 +21,24 @@ type MaybeInstrumentedDriver = Driver & { __sentry_instrumented__?: boolean; }; +/** + * Methods that should have a key argument. + */ +const KEYED_METHODS = new Set([ + 'hasItem', + 'getItem', + 'getItemRaw', + 'getItems', + 'setItem', + 'setItemRaw', + 'setItems', + 'removeItem', +]); + /** * Methods that should have a attribute to indicate a cache hit. */ -const KEYED_METHODS = new Set(['hasItem', 'getItem', 'getItemRaw', 'getItems']); +const CACHE_HIT_METHODS = new Set(['hasItem', 'getItem', 'getKeys']); /** * Creates a Nitro plugin that instruments the storage driver. @@ -70,6 +84,7 @@ function instrumentDriver(driver: MaybeInstrumentedDriver, mountBase: string): D debug.log(`[storage] Instrumenting driver: "${driver.name}" on mount: "${mountBase}"`); // List of driver methods to instrument + // get/set/remove are aliases and already use their {method}Item methods const methodsToInstrument: (keyof Driver)[] = [ 'hasItem', 'getItem', @@ -115,7 +130,9 @@ function createMethodWrapper( debug.log(`[storage] Running method: "${methodName}" on driver: "${driver.name ?? 'unknown'}"`); - const spanName = KEYED_METHODS.has(methodName) ? String(args?.[0]) : `storage.${normalizeMethodName(methodName)}`; + const spanName = KEYED_METHODS.has(methodName) + ? `${mountBase}${args?.[0]}` + : `storage.${normalizeMethodName(methodName)}`; return startSpan( { @@ -127,7 +144,7 @@ function createMethodWrapper( const result = await target.apply(thisArg, args); span.setStatus({ code: SPAN_STATUS_OK }); - if (KEYED_METHODS.has(methodName)) { + if (CACHE_HIT_METHODS.has(methodName)) { span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, true); } From af0e4b129994bf1c0b36737764e3b4fc0d8f7d3a Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 3 Oct 2025 18:18:24 +0300 Subject: [PATCH 08/13] fix: tighten types and added cache hit attr for get item raw --- packages/nuxt/src/runtime/plugins/storage.server.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/storage.server.ts b/packages/nuxt/src/runtime/plugins/storage.server.ts index d966f9077ef9..324c2e5e1e35 100644 --- a/packages/nuxt/src/runtime/plugins/storage.server.ts +++ b/packages/nuxt/src/runtime/plugins/storage.server.ts @@ -21,10 +21,12 @@ type MaybeInstrumentedDriver = Driver & { __sentry_instrumented__?: boolean; }; +type DriverMethod = keyof Driver; + /** * Methods that should have a key argument. */ -const KEYED_METHODS = new Set([ +const KEYED_METHODS = new Set([ 'hasItem', 'getItem', 'getItemRaw', @@ -38,7 +40,7 @@ const KEYED_METHODS = new Set([ /** * Methods that should have a attribute to indicate a cache hit. */ -const CACHE_HIT_METHODS = new Set(['hasItem', 'getItem', 'getKeys']); +const CACHE_HIT_METHODS = new Set(['hasItem', 'getItem', 'getItemRaw', 'getKeys']); /** * Creates a Nitro plugin that instruments the storage driver. @@ -85,7 +87,7 @@ function instrumentDriver(driver: MaybeInstrumentedDriver, mountBase: string): D // List of driver methods to instrument // get/set/remove are aliases and already use their {method}Item methods - const methodsToInstrument: (keyof Driver)[] = [ + const methodsToInstrument: DriverMethod[] = [ 'hasItem', 'getItem', 'getItemRaw', @@ -120,7 +122,7 @@ function instrumentDriver(driver: MaybeInstrumentedDriver, mountBase: string): D */ function createMethodWrapper( original: (...args: unknown[]) => unknown, - methodName: string, + methodName: DriverMethod, driver: Driver, mountBase: string, ): (...args: unknown[]) => unknown { From 50d545f5e3d35cae47a88db4fba1658dfef8aa3d Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 3 Oct 2025 18:21:34 +0300 Subject: [PATCH 09/13] tests: added e2e tests for storage instrumentation --- .../test-applications/nuxt-3/nuxt.config.ts | 7 + .../nuxt-3/server/api/storage-test.ts | 54 +++++++ .../nuxt-3/tests/storage.test.ts | 150 ++++++++++++++++++ .../test-applications/nuxt-4/nuxt.config.ts | 7 + .../nuxt-4/server/api/storage-test.ts | 54 +++++++ .../nuxt-4/tests/storage.test.ts | 150 ++++++++++++++++++ 6 files changed, 422 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/server/api/storage-test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/api/storage-test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts index 0fcccd560af9..8ea55702863c 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts @@ -11,4 +11,11 @@ export default defineNuxtConfig({ }, }, }, + nitro: { + storage: { + 'test-storage': { + driver: 'memory', + }, + }, + }, }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/storage-test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/storage-test.ts new file mode 100644 index 000000000000..f051daf59422 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/storage-test.ts @@ -0,0 +1,54 @@ +import { useStorage } from '#imports'; +import { defineEventHandler } from 'h3'; + +export default defineEventHandler(async _event => { + const storage = useStorage('test-storage'); + + // Test all instrumented methods + const results: Record = {}; + + // Test setItem + await storage.setItem('user:123', { name: 'John Doe', email: 'john@example.com' }); + results.setItem = 'success'; + + // Test setItemRaw + await storage.setItemRaw('raw:data', Buffer.from('raw data')); + results.setItemRaw = 'success'; + + // Manually set batch items (setItems not supported by memory driver) + await storage.setItem('batch:1', 'value1'); + await storage.setItem('batch:2', 'value2'); + + // Test hasItem + const hasUser = await storage.hasItem('user:123'); + results.hasItem = hasUser; + + // Test getItem + const user = await storage.getItem('user:123'); + results.getItem = user; + + // Test getItemRaw + const rawData = await storage.getItemRaw('raw:data'); + results.getItemRaw = rawData?.toString(); + + // Test getKeys + const keys = await storage.getKeys('batch:'); + results.getKeys = keys; + + // Test removeItem + await storage.removeItem('batch:1'); + results.removeItem = 'success'; + + // Test clear + await storage.clear(); + results.clear = 'success'; + + // Verify clear worked + const keysAfterClear = await storage.getKeys(); + results.keysAfterClear = keysAfterClear; + + return { + success: true, + results, + }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage.test.ts new file mode 100644 index 000000000000..3ff364cc53aa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage.test.ts @@ -0,0 +1,150 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SEMANTIC_ATTRIBUTE_CACHE_HIT, SEMANTIC_ATTRIBUTE_CACHE_KEY } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; + +test.describe('Storage Instrumentation', () => { + const prefixKey = (key: string) => `test-storage:${key}`; + + test('instruments all storage operations and creates spans with correct attributes', async ({ request }) => { + const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => { + return transactionEvent.transaction?.includes('GET /api/storage-test') ?? false; + }); + + const response = await request.get('/api/storage-test'); + expect(response.status()).toBe(200); + + const transaction = await transactionPromise; + + // Helper to find spans by operation + const findSpansByOp = (op: string) => { + return transaction.spans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === op) || []; + }; + + // Test setItem spans + const setItemSpans = findSpansByOp('cache.set_item'); + expect(setItemSpans.length).toBeGreaterThanOrEqual(1); + const setItemSpan = setItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); + expect(setItemSpan).toBeDefined(); + expect(setItemSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.set_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), + 'nuxt.storage.op': 'setItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + expect(setItemSpan?.description).toBe(prefixKey('user:123')); + + // Test setItemRaw spans + const setItemRawSpans = findSpansByOp('cache.set_item_raw'); + expect(setItemRawSpans.length).toBeGreaterThanOrEqual(1); + const setItemRawSpan = setItemRawSpans.find( + span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('raw:data'), + ); + expect(setItemRawSpan).toBeDefined(); + expect(setItemRawSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.set_item_raw', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('raw:data'), + 'nuxt.storage.op': 'setItemRaw', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Test hasItem spans - should have cache hit attribute + const hasItemSpans = findSpansByOp('cache.has_item'); + expect(hasItemSpans.length).toBeGreaterThanOrEqual(1); + const hasItemSpan = hasItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); + expect(hasItemSpan).toBeDefined(); + expect(hasItemSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.has_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, + 'nuxt.storage.op': 'hasItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Test getItem spans - should have cache hit attribute + const getItemSpans = findSpansByOp('cache.get_item'); + expect(getItemSpans.length).toBeGreaterThanOrEqual(1); + const getItemSpan = getItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); + expect(getItemSpan).toBeDefined(); + expect(getItemSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, + 'nuxt.storage.op': 'getItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + expect(getItemSpan?.description).toBe(prefixKey('user:123')); + + // Test getItemRaw spans - should have cache hit attribute + const getItemRawSpans = findSpansByOp('cache.get_item_raw'); + expect(getItemRawSpans.length).toBeGreaterThanOrEqual(1); + const getItemRawSpan = getItemRawSpans.find( + span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('raw:data'), + ); + expect(getItemRawSpan).toBeDefined(); + expect(getItemRawSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get_item_raw', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('raw:data'), + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, + 'nuxt.storage.op': 'getItemRaw', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Test getKeys spans + const getKeysSpans = findSpansByOp('cache.get_keys'); + expect(getKeysSpans.length).toBeGreaterThanOrEqual(1); + expect(getKeysSpans[0]?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get_keys', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + 'nuxt.storage.op': 'getKeys', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Test removeItem spans + const removeItemSpans = findSpansByOp('cache.remove_item'); + expect(removeItemSpans.length).toBeGreaterThanOrEqual(1); + const removeItemSpan = removeItemSpans.find( + span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('batch:1'), + ); + expect(removeItemSpan).toBeDefined(); + expect(removeItemSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('batch:1'), + 'nuxt.storage.op': 'removeItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Test clear spans + const clearSpans = findSpansByOp('cache.clear'); + expect(clearSpans.length).toBeGreaterThanOrEqual(1); + expect(clearSpans[0]?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.clear', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + 'nuxt.storage.op': 'clear', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Verify all spans have OK status + const allStorageSpans = transaction.spans?.filter( + span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt', + ); + expect(allStorageSpans?.length).toBeGreaterThan(0); + allStorageSpans?.forEach(span => { + expect(span.status).toBe('ok'); + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts index d0ae045f1e9d..50924877649d 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts @@ -21,4 +21,11 @@ export default defineNuxtConfig({ }, }, }, + nitro: { + storage: { + 'test-storage': { + driver: 'memory', + }, + }, + }, }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/storage-test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/storage-test.ts new file mode 100644 index 000000000000..f051daf59422 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/storage-test.ts @@ -0,0 +1,54 @@ +import { useStorage } from '#imports'; +import { defineEventHandler } from 'h3'; + +export default defineEventHandler(async _event => { + const storage = useStorage('test-storage'); + + // Test all instrumented methods + const results: Record = {}; + + // Test setItem + await storage.setItem('user:123', { name: 'John Doe', email: 'john@example.com' }); + results.setItem = 'success'; + + // Test setItemRaw + await storage.setItemRaw('raw:data', Buffer.from('raw data')); + results.setItemRaw = 'success'; + + // Manually set batch items (setItems not supported by memory driver) + await storage.setItem('batch:1', 'value1'); + await storage.setItem('batch:2', 'value2'); + + // Test hasItem + const hasUser = await storage.hasItem('user:123'); + results.hasItem = hasUser; + + // Test getItem + const user = await storage.getItem('user:123'); + results.getItem = user; + + // Test getItemRaw + const rawData = await storage.getItemRaw('raw:data'); + results.getItemRaw = rawData?.toString(); + + // Test getKeys + const keys = await storage.getKeys('batch:'); + results.getKeys = keys; + + // Test removeItem + await storage.removeItem('batch:1'); + results.removeItem = 'success'; + + // Test clear + await storage.clear(); + results.clear = 'success'; + + // Verify clear worked + const keysAfterClear = await storage.getKeys(); + results.keysAfterClear = keysAfterClear; + + return { + success: true, + results, + }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage.test.ts new file mode 100644 index 000000000000..5cfe8957ea0f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage.test.ts @@ -0,0 +1,150 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SEMANTIC_ATTRIBUTE_CACHE_HIT, SEMANTIC_ATTRIBUTE_CACHE_KEY } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; + +test.describe('Storage Instrumentation', () => { + const prefixKey = (key: string) => `test-storage:${key}`; + + test('instruments all storage operations and creates spans with correct attributes', async ({ request }) => { + const transactionPromise = waitForTransaction('nuxt-4', transactionEvent => { + return transactionEvent.transaction?.includes('GET /api/storage-test') ?? false; + }); + + const response = await request.get('/api/storage-test'); + expect(response.status()).toBe(200); + + const transaction = await transactionPromise; + + // Helper to find spans by operation + const findSpansByOp = (op: string) => { + return transaction.spans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === op) || []; + }; + + // Test setItem spans + const setItemSpans = findSpansByOp('cache.set_item'); + expect(setItemSpans.length).toBeGreaterThanOrEqual(1); + const setItemSpan = setItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); + expect(setItemSpan).toBeDefined(); + expect(setItemSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.set_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), + 'nuxt.storage.op': 'setItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + expect(setItemSpan?.description).toBe(prefixKey('user:123')); + + // Test setItemRaw spans + const setItemRawSpans = findSpansByOp('cache.set_item_raw'); + expect(setItemRawSpans.length).toBeGreaterThanOrEqual(1); + const setItemRawSpan = setItemRawSpans.find( + span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('raw:data'), + ); + expect(setItemRawSpan).toBeDefined(); + expect(setItemRawSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.set_item_raw', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('raw:data'), + 'nuxt.storage.op': 'setItemRaw', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Test hasItem spans - should have cache hit attribute + const hasItemSpans = findSpansByOp('cache.has_item'); + expect(hasItemSpans.length).toBeGreaterThanOrEqual(1); + const hasItemSpan = hasItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); + expect(hasItemSpan).toBeDefined(); + expect(hasItemSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.has_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, + 'nuxt.storage.op': 'hasItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Test getItem spans - should have cache hit attribute + const getItemSpans = findSpansByOp('cache.get_item'); + expect(getItemSpans.length).toBeGreaterThanOrEqual(1); + const getItemSpan = getItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); + expect(getItemSpan).toBeDefined(); + expect(getItemSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, + 'nuxt.storage.op': 'getItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + expect(getItemSpan?.description).toBe(prefixKey('user:123')); + + // Test getItemRaw spans - should have cache hit attribute + const getItemRawSpans = findSpansByOp('cache.get_item_raw'); + expect(getItemRawSpans.length).toBeGreaterThanOrEqual(1); + const getItemRawSpan = getItemRawSpans.find( + span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('raw:data'), + ); + expect(getItemRawSpan).toBeDefined(); + expect(getItemRawSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get_item_raw', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('raw:data'), + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, + 'nuxt.storage.op': 'getItemRaw', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Test getKeys spans + const getKeysSpans = findSpansByOp('cache.get_keys'); + expect(getKeysSpans.length).toBeGreaterThanOrEqual(1); + expect(getKeysSpans[0]?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get_keys', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + 'nuxt.storage.op': 'getKeys', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Test removeItem spans + const removeItemSpans = findSpansByOp('cache.remove_item'); + expect(removeItemSpans.length).toBeGreaterThanOrEqual(1); + const removeItemSpan = removeItemSpans.find( + span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('batch:1'), + ); + expect(removeItemSpan).toBeDefined(); + expect(removeItemSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('batch:1'), + 'nuxt.storage.op': 'removeItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Test clear spans + const clearSpans = findSpansByOp('cache.clear'); + expect(clearSpans.length).toBeGreaterThanOrEqual(1); + expect(clearSpans[0]?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.clear', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + 'nuxt.storage.op': 'clear', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Verify all spans have OK status + const allStorageSpans = transaction.spans?.filter( + span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt', + ); + expect(allStorageSpans?.length).toBeGreaterThan(0); + allStorageSpans?.forEach(span => { + expect(span.status).toBe('ok'); + }); + }); +}); From cf873e00fd726fd580c6f18052a53e2cedcc8a3f Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 3 Oct 2025 18:25:58 +0300 Subject: [PATCH 10/13] tests: make sure the storage aliases are covered --- .../nuxt-3/server/api/storage-aliases-test.ts | 46 ++++++++ .../nuxt-3/tests/storage-aliases.test.ts | 107 ++++++++++++++++++ .../nuxt-4/server/api/storage-aliases-test.ts | 46 ++++++++ .../nuxt-4/tests/storage-aliases.test.ts | 107 ++++++++++++++++++ 4 files changed, 306 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/server/api/storage-aliases-test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage-aliases.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/api/storage-aliases-test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage-aliases.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/storage-aliases-test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/storage-aliases-test.ts new file mode 100644 index 000000000000..e204453d1000 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/storage-aliases-test.ts @@ -0,0 +1,46 @@ +import { useStorage } from '#imports'; +import { defineEventHandler } from 'h3'; + +export default defineEventHandler(async _event => { + const storage = useStorage('test-storage'); + + // Test all alias methods (get, set, del, remove) + const results: Record = {}; + + // Test set (alias for setItem) + await storage.set('alias:user', { name: 'Jane Doe', role: 'admin' }); + results.set = 'success'; + + // Test get (alias for getItem) + const user = await storage.get('alias:user'); + results.get = user; + + // Test has (alias for hasItem) + const hasUser = await storage.has('alias:user'); + results.has = hasUser; + + // Setup for delete tests + await storage.set('alias:temp1', 'temp1'); + await storage.set('alias:temp2', 'temp2'); + + // Test del (alias for removeItem) + await storage.del('alias:temp1'); + results.del = 'success'; + + // Test remove (alias for removeItem) + await storage.remove('alias:temp2'); + results.remove = 'success'; + + // Verify deletions worked + const hasTemp1 = await storage.has('alias:temp1'); + const hasTemp2 = await storage.has('alias:temp2'); + results.verifyDeletions = !hasTemp1 && !hasTemp2; + + // Clean up + await storage.clear(); + + return { + success: true, + results, + }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage-aliases.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage-aliases.test.ts new file mode 100644 index 000000000000..cbfbb909327f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage-aliases.test.ts @@ -0,0 +1,107 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SEMANTIC_ATTRIBUTE_CACHE_HIT, SEMANTIC_ATTRIBUTE_CACHE_KEY } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; + +test.describe('Storage Instrumentation - Aliases', () => { + const prefixKey = (key: string) => `test-storage:${key}`; + + test('instruments storage alias methods (get, set, has, del, remove) and creates spans', async ({ request }) => { + const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => { + return transactionEvent.transaction?.includes('GET /api/storage-aliases-test') ?? false; + }); + + const response = await request.get('/api/storage-aliases-test'); + expect(response.status()).toBe(200); + + const transaction = await transactionPromise; + + // Helper to find spans by operation + const findSpansByOp = (op: string) => { + return transaction.spans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === op) || []; + }; + + // Test set (alias for setItem) + const setSpans = findSpansByOp('cache.set_item'); + expect(setSpans.length).toBeGreaterThanOrEqual(1); + const setSpan = setSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); + expect(setSpan).toBeDefined(); + expect(setSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.set_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), + 'nuxt.storage.op': 'setItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + expect(setSpan?.description).toBe(prefixKey('alias:user')); + + // Test get (alias for getItem) + const getSpans = findSpansByOp('cache.get_item'); + expect(getSpans.length).toBeGreaterThanOrEqual(1); + const getSpan = getSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); + expect(getSpan).toBeDefined(); + expect(getSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, + 'nuxt.storage.op': 'getItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + expect(getSpan?.description).toBe(prefixKey('alias:user')); + + // Test has (alias for hasItem) + const hasSpans = findSpansByOp('cache.has_item'); + expect(hasSpans.length).toBeGreaterThanOrEqual(1); + const hasSpan = hasSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); + expect(hasSpan).toBeDefined(); + expect(hasSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.has_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, + 'nuxt.storage.op': 'hasItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Test del and remove (both aliases for removeItem) + const removeSpans = findSpansByOp('cache.remove_item'); + expect(removeSpans.length).toBeGreaterThanOrEqual(2); // Should have both del and remove calls + + const delSpan = removeSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:temp1')); + expect(delSpan).toBeDefined(); + expect(delSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:temp1'), + 'nuxt.storage.op': 'removeItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + expect(delSpan?.description).toBe(prefixKey('alias:temp1')); + + const removeSpan = removeSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:temp2')); + expect(removeSpan).toBeDefined(); + expect(removeSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:temp2'), + 'nuxt.storage.op': 'removeItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + expect(removeSpan?.description).toBe(prefixKey('alias:temp2')); + + // Verify all spans have OK status + const allStorageSpans = transaction.spans?.filter( + span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt', + ); + expect(allStorageSpans?.length).toBeGreaterThan(0); + allStorageSpans?.forEach(span => { + expect(span.status).toBe('ok'); + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/storage-aliases-test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/storage-aliases-test.ts new file mode 100644 index 000000000000..e204453d1000 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/storage-aliases-test.ts @@ -0,0 +1,46 @@ +import { useStorage } from '#imports'; +import { defineEventHandler } from 'h3'; + +export default defineEventHandler(async _event => { + const storage = useStorage('test-storage'); + + // Test all alias methods (get, set, del, remove) + const results: Record = {}; + + // Test set (alias for setItem) + await storage.set('alias:user', { name: 'Jane Doe', role: 'admin' }); + results.set = 'success'; + + // Test get (alias for getItem) + const user = await storage.get('alias:user'); + results.get = user; + + // Test has (alias for hasItem) + const hasUser = await storage.has('alias:user'); + results.has = hasUser; + + // Setup for delete tests + await storage.set('alias:temp1', 'temp1'); + await storage.set('alias:temp2', 'temp2'); + + // Test del (alias for removeItem) + await storage.del('alias:temp1'); + results.del = 'success'; + + // Test remove (alias for removeItem) + await storage.remove('alias:temp2'); + results.remove = 'success'; + + // Verify deletions worked + const hasTemp1 = await storage.has('alias:temp1'); + const hasTemp2 = await storage.has('alias:temp2'); + results.verifyDeletions = !hasTemp1 && !hasTemp2; + + // Clean up + await storage.clear(); + + return { + success: true, + results, + }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage-aliases.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage-aliases.test.ts new file mode 100644 index 000000000000..872381c13777 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage-aliases.test.ts @@ -0,0 +1,107 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SEMANTIC_ATTRIBUTE_CACHE_HIT, SEMANTIC_ATTRIBUTE_CACHE_KEY } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; + +test.describe('Storage Instrumentation - Aliases', () => { + const prefixKey = (key: string) => `test-storage:${key}`; + + test('instruments storage alias methods (get, set, has, del, remove) and creates spans', async ({ request }) => { + const transactionPromise = waitForTransaction('nuxt-4', transactionEvent => { + return transactionEvent.transaction?.includes('GET /api/storage-aliases-test') ?? false; + }); + + const response = await request.get('/api/storage-aliases-test'); + expect(response.status()).toBe(200); + + const transaction = await transactionPromise; + + // Helper to find spans by operation + const findSpansByOp = (op: string) => { + return transaction.spans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === op) || []; + }; + + // Test set (alias for setItem) + const setSpans = findSpansByOp('cache.set_item'); + expect(setSpans.length).toBeGreaterThanOrEqual(1); + const setSpan = setSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); + expect(setSpan).toBeDefined(); + expect(setSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.set_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), + 'nuxt.storage.op': 'setItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + expect(setSpan?.description).toBe(prefixKey('alias:user')); + + // Test get (alias for getItem) + const getSpans = findSpansByOp('cache.get_item'); + expect(getSpans.length).toBeGreaterThanOrEqual(1); + const getSpan = getSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); + expect(getSpan).toBeDefined(); + expect(getSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, + 'nuxt.storage.op': 'getItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + expect(getSpan?.description).toBe(prefixKey('alias:user')); + + // Test has (alias for hasItem) + const hasSpans = findSpansByOp('cache.has_item'); + expect(hasSpans.length).toBeGreaterThanOrEqual(1); + const hasSpan = hasSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); + expect(hasSpan).toBeDefined(); + expect(hasSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.has_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), + [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, + 'nuxt.storage.op': 'hasItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + + // Test del and remove (both aliases for removeItem) + const removeSpans = findSpansByOp('cache.remove_item'); + expect(removeSpans.length).toBeGreaterThanOrEqual(2); // Should have both del and remove calls + + const delSpan = removeSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:temp1')); + expect(delSpan).toBeDefined(); + expect(delSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:temp1'), + 'nuxt.storage.op': 'removeItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + expect(delSpan?.description).toBe(prefixKey('alias:temp1')); + + const removeSpan = removeSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:temp2')); + expect(removeSpan).toBeDefined(); + expect(removeSpan?.data).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove_item', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:temp2'), + 'nuxt.storage.op': 'removeItem', + 'nuxt.storage.mount': 'test-storage:', + 'nuxt.storage.driver': 'memory', + }); + expect(removeSpan?.description).toBe(prefixKey('alias:temp2')); + + // Verify all spans have OK status + const allStorageSpans = transaction.spans?.filter( + span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt', + ); + expect(allStorageSpans?.length).toBeGreaterThan(0); + allStorageSpans?.forEach(span => { + expect(span.status).toBe('ok'); + }); + }); +}); From 14cc7391172d7c6a9f34fd04301b5fbdc44fc840 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 3 Oct 2025 18:55:23 +0300 Subject: [PATCH 11/13] fix: es compat --- packages/nuxt/src/common/server-template.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/common/server-template.ts b/packages/nuxt/src/common/server-template.ts index 7f0fafe6d8cb..afdb46345d5c 100644 --- a/packages/nuxt/src/common/server-template.ts +++ b/packages/nuxt/src/common/server-template.ts @@ -9,7 +9,7 @@ import type { NuxtTemplate } from 'nuxt/schema'; export function addServerTemplate(template: NuxtTemplate): NuxtTemplate { const nuxt = useNuxt(); if (template.filename) { - nuxt.options.nitro.virtual ||= {}; + nuxt.options.nitro.virtual = nuxt.options.nitro.virtual || {}; nuxt.options.nitro.virtual[template.filename] = template.getContents; } From fe203a51eaf39263976e1cc4f002412fa7734ab6 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Sat, 4 Oct 2025 09:20:12 +0300 Subject: [PATCH 12/13] tests: avoid importing semantic attrs exported by indirect dep --- .../test-applications/nuxt-3/tests/storage-aliases.test.ts | 3 ++- .../e2e-tests/test-applications/nuxt-3/tests/storage.test.ts | 3 ++- .../test-applications/nuxt-4/tests/storage-aliases.test.ts | 3 ++- .../e2e-tests/test-applications/nuxt-4/tests/storage.test.ts | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage-aliases.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage-aliases.test.ts index cbfbb909327f..f5da20313c71 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage-aliases.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage-aliases.test.ts @@ -1,10 +1,11 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_CACHE_HIT, SEMANTIC_ATTRIBUTE_CACHE_KEY } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test.describe('Storage Instrumentation - Aliases', () => { const prefixKey = (key: string) => `test-storage:${key}`; + const SEMANTIC_ATTRIBUTE_CACHE_KEY = 'cache.key'; + const SEMANTIC_ATTRIBUTE_CACHE_HIT = 'cache.hit'; test('instruments storage alias methods (get, set, has, del, remove) and creates spans', async ({ request }) => { const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage.test.ts index 3ff364cc53aa..cc9d2957d122 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage.test.ts @@ -1,10 +1,11 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_CACHE_HIT, SEMANTIC_ATTRIBUTE_CACHE_KEY } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test.describe('Storage Instrumentation', () => { const prefixKey = (key: string) => `test-storage:${key}`; + const SEMANTIC_ATTRIBUTE_CACHE_KEY = 'cache.key'; + const SEMANTIC_ATTRIBUTE_CACHE_HIT = 'cache.hit'; test('instruments all storage operations and creates spans with correct attributes', async ({ request }) => { const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage-aliases.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage-aliases.test.ts index 872381c13777..32074b4beccc 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage-aliases.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage-aliases.test.ts @@ -1,10 +1,11 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_CACHE_HIT, SEMANTIC_ATTRIBUTE_CACHE_KEY } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test.describe('Storage Instrumentation - Aliases', () => { const prefixKey = (key: string) => `test-storage:${key}`; + const SEMANTIC_ATTRIBUTE_CACHE_KEY = 'cache.key'; + const SEMANTIC_ATTRIBUTE_CACHE_HIT = 'cache.hit'; test('instruments storage alias methods (get, set, has, del, remove) and creates spans', async ({ request }) => { const transactionPromise = waitForTransaction('nuxt-4', transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage.test.ts index 5cfe8957ea0f..103725ea61b6 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/storage.test.ts @@ -1,10 +1,11 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_CACHE_HIT, SEMANTIC_ATTRIBUTE_CACHE_KEY } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test.describe('Storage Instrumentation', () => { const prefixKey = (key: string) => `test-storage:${key}`; + const SEMANTIC_ATTRIBUTE_CACHE_KEY = 'cache.key'; + const SEMANTIC_ATTRIBUTE_CACHE_HIT = 'cache.hit'; test('instruments all storage operations and creates spans with correct attributes', async ({ request }) => { const transactionPromise = waitForTransaction('nuxt-4', transactionEvent => { From 911b7fea4b4b0c3c0edd08493b022c8c7f59a9b8 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 6 Oct 2025 15:38:05 +0300 Subject: [PATCH 13/13] fix: cache hit detection and excessive mount instrumentation --- .../nuxt/src/runtime/plugins/storage.server.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/storage.server.ts b/packages/nuxt/src/runtime/plugins/storage.server.ts index 324c2e5e1e35..c5b33e54b4e6 100644 --- a/packages/nuxt/src/runtime/plugins/storage.server.ts +++ b/packages/nuxt/src/runtime/plugins/storage.server.ts @@ -40,7 +40,7 @@ const KEYED_METHODS = new Set([ /** * Methods that should have a attribute to indicate a cache hit. */ -const CACHE_HIT_METHODS = new Set(['hasItem', 'getItem', 'getItemRaw', 'getKeys']); +const CACHE_HIT_METHODS = new Set(['hasItem', 'getItem', 'getItemRaw']); /** * Creates a Nitro plugin that instruments the storage driver. @@ -66,10 +66,10 @@ export default defineNitroPlugin(async _nitroApp => { } catch { debug.error(`[storage] Failed to unmount mount: "${mount.base}"`); } - - // Wrap the mount method to instrument future mounts - storage.mount = wrapStorageMount(storage); } + + // Wrap the mount method to instrument future mounts + storage.mount = wrapStorageMount(storage); }); /** @@ -147,7 +147,7 @@ function createMethodWrapper( span.setStatus({ code: SPAN_STATUS_OK }); if (CACHE_HIT_METHODS.has(methodName)) { - span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, true); + span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, !isEmptyValue(result)); } return result; @@ -214,3 +214,10 @@ function getSpanAttributes(methodName: string, driver: Driver, mountBase: string function normalizeMethodName(methodName: string): string { return methodName.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); } + +/** + * Checks if the value is empty, used for cache hit detection. + */ +function isEmptyValue(value: unknown): boolean { + return value === null || value === undefined; +}