Skip to content
Merged
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
292389a
feat(nuxt): instrument storage drivers
logaretm Oct 2, 2025
de34b14
refactor: only instrument user-defined storage
logaretm Oct 2, 2025
267de2e
feat: added more logs and instrument drivers in-place
logaretm Oct 3, 2025
7e3c366
refactor: shorter logging messages
logaretm Oct 3, 2025
9b6ed5f
feat: remove disposed from instrumentation list
logaretm Oct 3, 2025
d7ab5d8
feat: clean up attributes and use semantic cache ops
logaretm Oct 3, 2025
9041dba
fix: show global key path on span name and adjust operation attrs
logaretm Oct 3, 2025
47c785c
fix: tighten types and added cache hit attr for get item raw
logaretm Oct 3, 2025
59a1196
tests: added e2e tests for storage instrumentation
logaretm Oct 3, 2025
bf92b1d
tests: make sure the storage aliases are covered
logaretm Oct 3, 2025
0070251
fix: es compat
logaretm Oct 3, 2025
64c39b1
tests: avoid importing semantic attrs exported by indirect dep
logaretm Oct 4, 2025
3b1f15d
fix: cache hit detection and excessive mount instrumentation
logaretm Oct 6, 2025
0617781
fix: protect storage mount() from nested instrumentation for sanity c…
logaretm Oct 8, 2025
561bd74
fix: remove uneeded try catch block
logaretm Oct 8, 2025
29ec356
refactor: move server-template util to vendor
logaretm Oct 10, 2025
e86d08c
fix: only log when DEBUG_BUILD flag is on
logaretm Oct 10, 2025
471482e
fix: use semantic span attributes for driver info
logaretm Oct 10, 2025
89bbf93
fix: handle multiple cache keys and more complex calls
logaretm Oct 10, 2025
e3c8e92
fix: revert log stuff
logaretm Oct 10, 2025
e717ce9
tests: updated test assertions
logaretm Oct 10, 2025
4d83901
fix: op name will contain the method name anyways
logaretm Oct 10, 2025
3aa4a31
fix: adjustment to span names
logaretm Oct 10, 2025
2372e91
fix: normalize cache keys properly
logaretm Oct 10, 2025
d621605
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