Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f9500c4
feat(nuxt): instrument storage drivers
logaretm Oct 2, 2025
48f2dcb
refactor: only instrument user-defined storage
logaretm Oct 2, 2025
1012674
feat: added more logs and instrument drivers in-place
logaretm Oct 3, 2025
b8d605b
refactor: shorter logging messages
logaretm Oct 3, 2025
b4aca20
feat: remove disposed from instrumentation list
logaretm Oct 3, 2025
dac291b
feat: clean up attributes and use semantic cache ops
logaretm Oct 3, 2025
f785c77
fix: show global key path on span name and adjust operation attrs
logaretm Oct 3, 2025
d5b317a
fix: tighten types and added cache hit attr for get item raw
logaretm Oct 3, 2025
e05bf53
tests: added e2e tests for storage instrumentation
logaretm Oct 3, 2025
0998d46
tests: make sure the storage aliases are covered
logaretm Oct 3, 2025
334428e
fix: es compat
logaretm Oct 3, 2025
572add6
tests: avoid importing semantic attrs exported by indirect dep
logaretm Oct 4, 2025
86dc654
fix: cache hit detection and excessive mount instrumentation
logaretm Oct 6, 2025
617d46b
fix: protect storage mount() from nested instrumentation for sanity c…
logaretm Oct 8, 2025
9f0fd65
fix: remove uneeded try catch block
logaretm Oct 8, 2025
97c1e00
refactor: move server-template util to vendor
logaretm Oct 10, 2025
c9a1845
fix: only log when DEBUG_BUILD flag is on
logaretm Oct 10, 2025
a682a70
fix: use semantic span attributes for driver info
logaretm Oct 10, 2025
c43af8f
fix: handle multiple cache keys and more complex calls
logaretm Oct 10, 2025
19b7d8d
fix: revert log stuff
logaretm Oct 10, 2025
7d03f1c
tests: updated test assertions
logaretm Oct 10, 2025
e9505da
fix: op name will contain the method name anyways
logaretm Oct 10, 2025
66bb5f3
fix: adjustment to span names
logaretm Oct 10, 2025
c1e4c0e
fix: normalize cache keys properly
logaretm Oct 10, 2025
d943f7d
tests: update assertions to match new keys and attr names
logaretm Oct 10, 2025
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 @@ -11,4 +11,11 @@ export default defineNuxtConfig({
},
},
},
nitro: {
storage: {
'test-storage': {
driver: 'memory',
},
},
},
});
Original file line number Diff line number Diff line change
@@ -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<string, unknown> = {};

// 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,
};
});
Original file line number Diff line number Diff line change
@@ -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<string, unknown> = {};

// 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,
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
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 => {
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'),
'db.operation.name': 'setItem',
'db.collection.name': 'test-storage',
'db.system.name': '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,
'db.operation.name': 'getItem',
'db.collection.name': 'test-storage',
'db.system.name': '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,
'db.operation.name': 'hasItem',
'db.collection.name': 'test-storage',
'db.system.name': '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'),
'db.operation.name': 'removeItem',
'db.collection.name': 'test-storage',
'db.system.name': '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'),
'db.operation.name': 'removeItem',
'db.collection.name': 'test-storage',
'db.system.name': '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');
});
});
});
154 changes: 154 additions & 0 deletions dev-packages/e2e-tests/test-applications/nuxt-3/tests/storage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
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 => {
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'),
'db.operation.name': 'setItem',
'db.collection.name': 'test-storage',
'db.system.name': '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'),
'db.operation.name': 'setItemRaw',
'db.collection.name': 'test-storage',
'db.system.name': '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,
'db.operation.name': 'hasItem',
'db.collection.name': 'test-storage',
'db.system.name': '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,
'db.operation.name': 'getItem',
'db.collection.name': 'test-storage',
'db.system.name': '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,
'db.operation.name': 'getItemRaw',
'db.collection.name': 'test-storage',
'db.system.name': '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',
'db.operation.name': 'getKeys',
'db.collection.name': 'test-storage',
'db.system.name': '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'),
'db.operation.name': 'removeItem',
'db.collection.name': 'test-storage',
'db.system.name': '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',
'db.operation.name': 'clear',
'db.collection.name': 'test-storage',
'db.system.name': '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');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,11 @@ export default defineNuxtConfig({
},
},
},
nitro: {
storage: {
'test-storage': {
driver: 'memory',
},
},
},
});
Loading