Skip to content

Commit a0eddae

Browse files
committed
feat(nuxt): instrument storage drivers
1 parent a524022 commit a0eddae

File tree

2 files changed

+146
-0
lines changed

2 files changed

+146
-0
lines changed

packages/nuxt/src/module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export default defineNuxtModule<ModuleOptions>({
9090

9191
if (serverConfigFile) {
9292
addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server'));
93+
addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/storage.server'));
9394

9495
addPlugin({
9596
src: moduleDirResolver.resolve('./runtime/plugins/route-detector.server'),
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import {
2+
type SpanAttributes,
3+
captureException,
4+
debug,
5+
flushIfServerless,
6+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
7+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
8+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
9+
SPAN_STATUS_ERROR,
10+
SPAN_STATUS_OK,
11+
startSpan,
12+
} from '@sentry/core';
13+
// eslint-disable-next-line import/no-extraneous-dependencies
14+
import { defineNitroPlugin, useStorage } from 'nitropack/runtime';
15+
import type { Driver } from 'unstorage';
16+
17+
/**
18+
* Creates a Nitro plugin that instruments the storage driver.
19+
*/
20+
export default defineNitroPlugin(async _nitroApp => {
21+
// This runs at runtime when the Nitro server starts
22+
const storage = useStorage();
23+
24+
// exclude mounts that are not relevant for instrumentation for a few reasons:
25+
// Nitro mounts some development-only mount points that are not relevant for instrumentation
26+
// https://nitro.build/guide/storage#development-only-mount-points
27+
const excludeMounts = new Set(['build:', 'cache:', 'root:', 'data:', 'src:', 'assets:']);
28+
29+
debug.log('[Storage Instrumentation] Starting to instrument storage drivers...');
30+
31+
// Get all mounted storage drivers
32+
const mounts = storage.getMounts();
33+
for (const mount of mounts) {
34+
// Skip excluded mounts and root mount
35+
if (!mount.base || excludeMounts.has(mount.base)) {
36+
continue;
37+
}
38+
39+
debug.log(`[Storage Instrumentation] Instrumenting mount: "${mount.base}"`);
40+
41+
const driver = instrumentDriver(mount.driver, mount.base);
42+
43+
try {
44+
// Remount with instrumented driver
45+
await storage.unmount(mount.base);
46+
await storage.mount(mount.base, driver);
47+
} catch {
48+
debug.error(`[Storage Instrumentation] Failed to unmount mount: "${mount.base}"`);
49+
}
50+
}
51+
});
52+
53+
/**
54+
* Instruments a driver by wrapping all method calls using proxies.
55+
*/
56+
function instrumentDriver(driver: Driver, mountBase: string): Driver {
57+
// List of driver methods to instrument
58+
const methodsToInstrument: (keyof Driver)[] = [
59+
'hasItem',
60+
'getItem',
61+
'getItemRaw',
62+
'getItems',
63+
'setItem',
64+
'setItemRaw',
65+
'setItems',
66+
'removeItem',
67+
'getKeys',
68+
'getMeta',
69+
'clear',
70+
'dispose',
71+
];
72+
73+
for (const methodName of methodsToInstrument) {
74+
const original = driver[methodName];
75+
// Skip if method doesn't exist on this driver
76+
if (typeof original !== 'function') {
77+
continue;
78+
}
79+
80+
// Replace with instrumented
81+
driver[methodName] = createMethodWrapper(original, methodName, driver.name ?? 'unknown', mountBase);
82+
}
83+
84+
return driver;
85+
}
86+
87+
/**
88+
* Creates an instrumented method for the given method.
89+
*/
90+
function createMethodWrapper(
91+
original: (...args: unknown[]) => unknown,
92+
methodName: string,
93+
driverName: string,
94+
mountBase: string,
95+
): (...args: unknown[]) => unknown {
96+
return new Proxy(original, {
97+
async apply(target, thisArg, args) {
98+
const attributes = getSpanAttributes(methodName, driverName ?? 'unknown', mountBase);
99+
100+
return startSpan(
101+
{
102+
name: `storage.${methodName}`,
103+
attributes,
104+
},
105+
async span => {
106+
try {
107+
const result = await target.apply(thisArg, args);
108+
span.setStatus({ code: SPAN_STATUS_OK });
109+
110+
return result;
111+
} catch (error) {
112+
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
113+
captureException(error, {
114+
mechanism: {
115+
handled: false,
116+
type: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN],
117+
},
118+
});
119+
120+
// Re-throw the error to be handled by the caller
121+
throw error;
122+
} finally {
123+
await flushIfServerless();
124+
}
125+
},
126+
);
127+
},
128+
});
129+
}
130+
131+
/**
132+
* Gets the span attributes for the storage method.
133+
*/
134+
function getSpanAttributes(methodName: string, driverName: string, mountBase: string): SpanAttributes {
135+
const attributes: SpanAttributes = {
136+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'app.storage.nuxt',
137+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
138+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.app.storage.nuxt',
139+
'nuxt.storage.op': methodName,
140+
'nuxt.storage.driver': driverName,
141+
'nuxt.storage.mount': mountBase,
142+
};
143+
144+
return attributes;
145+
}

0 commit comments

Comments
 (0)