Skip to content

Commit 4f26f70

Browse files
committed
tests: added e2e cache tests
1 parent cf715eb commit 4f26f70

File tree

4 files changed

+490
-0
lines changed

4 files changed

+490
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { cachedFunction, defineCachedEventHandler, defineEventHandler, getQuery } from '#imports';
2+
3+
// Test cachedFunction
4+
const getCachedUser = cachedFunction(
5+
async (userId: string) => {
6+
return {
7+
id: userId,
8+
name: `User ${userId}`,
9+
email: `user${userId}@example.com`,
10+
timestamp: Date.now(),
11+
};
12+
},
13+
{
14+
maxAge: 60,
15+
name: 'getCachedUser',
16+
getKey: (userId: string) => `user:${userId}`,
17+
},
18+
);
19+
20+
// Test cachedFunction with different options
21+
const getCachedData = cachedFunction(
22+
async (key: string) => {
23+
return {
24+
key,
25+
value: `cached-value-${key}`,
26+
timestamp: Date.now(),
27+
};
28+
},
29+
{
30+
maxAge: 120,
31+
name: 'getCachedData',
32+
getKey: (key: string) => `data:${key}`,
33+
},
34+
);
35+
36+
// Test defineCachedEventHandler
37+
const cachedHandler = defineCachedEventHandler(
38+
async event => {
39+
return {
40+
message: 'This response is cached',
41+
timestamp: Date.now(),
42+
path: event.path,
43+
};
44+
},
45+
{
46+
maxAge: 60,
47+
name: 'cachedHandler',
48+
},
49+
);
50+
51+
export default defineEventHandler(async event => {
52+
const results: Record<string, unknown> = {};
53+
const testKey = String(getQuery(event).user ?? '');
54+
const dataKey = String(getQuery(event).data ?? '');
55+
56+
// Test cachedFunction - first call (cache miss)
57+
const user1 = await getCachedUser(testKey);
58+
results.cachedUser1 = user1;
59+
60+
// Test cachedFunction - second call (cache hit)
61+
const user2 = await getCachedUser(testKey);
62+
results.cachedUser2 = user2;
63+
64+
// Test cachedFunction with different key (cache miss)
65+
const user3 = await getCachedUser(`${testKey}456`);
66+
results.cachedUser3 = user3;
67+
68+
// Test another cachedFunction
69+
const data1 = await getCachedData(dataKey);
70+
results.cachedData1 = data1;
71+
72+
// Test cachedFunction - cache hit
73+
const data2 = await getCachedData(dataKey);
74+
results.cachedData2 = data2;
75+
76+
// Test cachedEventHandler by calling it
77+
const cachedResponse = await cachedHandler(event);
78+
results.cachedResponse = cachedResponse;
79+
80+
return {
81+
success: true,
82+
results,
83+
};
84+
});
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt';
4+
5+
test.describe('Cache Instrumentation', () => {
6+
const SEMANTIC_ATTRIBUTE_CACHE_KEY = 'cache.key';
7+
const SEMANTIC_ATTRIBUTE_CACHE_HIT = 'cache.hit';
8+
9+
test('instruments cachedFunction and cachedEventHandler calls and creates spans with correct attributes', async ({
10+
request,
11+
}) => {
12+
const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => {
13+
return transactionEvent.transaction?.includes('GET /api/cache-test') ?? false;
14+
});
15+
16+
const response = await request.get('/api/cache-test?user=123&data=test-key');
17+
expect(response.status()).toBe(200);
18+
19+
const transaction = await transactionPromise;
20+
21+
// Helper to find spans by operation
22+
const findSpansByOp = (op: string) => {
23+
return transaction.spans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === op) || [];
24+
};
25+
26+
// Test that we have cache operations from cachedFunction and cachedEventHandler
27+
const allCacheSpans = transaction.spans?.filter(
28+
span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt',
29+
);
30+
expect(allCacheSpans?.length).toBeGreaterThan(0);
31+
32+
// Test getItem spans for cachedFunction - should have both cache miss and cache hit
33+
const getItemSpans = findSpansByOp('cache.get_item');
34+
expect(getItemSpans.length).toBeGreaterThan(0);
35+
36+
// Find cache miss (first call to getCachedUser('123'))
37+
const cacheMissSpan = getItemSpans.find(
38+
span =>
39+
typeof span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === 'string' &&
40+
span.data[SEMANTIC_ATTRIBUTE_CACHE_KEY].includes('user:123') &&
41+
!span.data?.[SEMANTIC_ATTRIBUTE_CACHE_HIT],
42+
);
43+
if (cacheMissSpan) {
44+
expect(cacheMissSpan.data).toMatchObject({
45+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get_item',
46+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt',
47+
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: false,
48+
'nuxt.storage.op': 'getItem',
49+
'nuxt.storage.mount': expect.stringMatching(/^(cache:)?$/),
50+
});
51+
}
52+
53+
// Find cache hit (second call to getCachedUser('123'))
54+
const cacheHitSpan = getItemSpans.find(
55+
span =>
56+
typeof span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === 'string' &&
57+
span.data[SEMANTIC_ATTRIBUTE_CACHE_KEY].includes('user:123') &&
58+
span.data?.[SEMANTIC_ATTRIBUTE_CACHE_HIT],
59+
);
60+
if (cacheHitSpan) {
61+
expect(cacheHitSpan.data).toMatchObject({
62+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get_item',
63+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt',
64+
[SEMANTIC_ATTRIBUTE_CACHE_HIT]: true,
65+
'nuxt.storage.op': 'getItem',
66+
'nuxt.storage.mount': expect.stringMatching(/^(cache:)?$/),
67+
});
68+
}
69+
70+
// Test setItem spans for cachedFunction - when cache miss occurs, value is set
71+
const setItemSpans = findSpansByOp('cache.set_item');
72+
expect(setItemSpans.length).toBeGreaterThan(0);
73+
74+
const cacheSetSpan = setItemSpans.find(
75+
span =>
76+
typeof span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === 'string' &&
77+
span.data[SEMANTIC_ATTRIBUTE_CACHE_KEY].includes('user:123'),
78+
);
79+
if (cacheSetSpan) {
80+
expect(cacheSetSpan.data).toMatchObject({
81+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.set_item',
82+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt',
83+
'nuxt.storage.op': 'setItem',
84+
'nuxt.storage.mount': expect.stringMatching(/^(cache:)?$/),
85+
});
86+
}
87+
88+
// Test that we have spans for different cached functions
89+
const dataKeySpans = getItemSpans.filter(
90+
span =>
91+
typeof span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === 'string' &&
92+
span.data[SEMANTIC_ATTRIBUTE_CACHE_KEY].includes('data:test-key'),
93+
);
94+
expect(dataKeySpans.length).toBeGreaterThan(0);
95+
96+
// Test that we have spans for cachedEventHandler
97+
const cachedHandlerSpans = getItemSpans.filter(
98+
span =>
99+
typeof span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === 'string' &&
100+
span.data[SEMANTIC_ATTRIBUTE_CACHE_KEY].includes('cachedHandler'),
101+
);
102+
expect(cachedHandlerSpans.length).toBeGreaterThan(0);
103+
104+
// Verify all cache spans have OK status
105+
allCacheSpans?.forEach(span => {
106+
expect(span.status).toBe('ok');
107+
});
108+
109+
// Verify cache spans are properly nested under the transaction
110+
allCacheSpans?.forEach(span => {
111+
expect(span.parent_span_id).toBeDefined();
112+
});
113+
});
114+
115+
test('correctly tracks cache hits and misses for cachedFunction', async ({ request }) => {
116+
// Use a unique key for this test to ensure fresh cache state
117+
const uniqueUser = `test-${Date.now()}`;
118+
const uniqueData = `data-${Date.now()}`;
119+
120+
const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => {
121+
return transactionEvent.transaction?.includes('GET /api/cache-test') ?? false;
122+
});
123+
124+
await request.get(`/api/cache-test?user=${uniqueUser}&data=${uniqueData}`);
125+
const transaction1 = await transactionPromise;
126+
127+
// Get all cache-related spans
128+
const allCacheSpans = transaction1.spans?.filter(
129+
span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt',
130+
);
131+
132+
// We should have cache operations
133+
expect(allCacheSpans?.length).toBeGreaterThan(0);
134+
135+
// Get all getItem operations
136+
const allGetItemSpans = allCacheSpans?.filter(
137+
span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'cache.get_item',
138+
);
139+
140+
// Get all setItem operations
141+
const allSetItemSpans = allCacheSpans?.filter(
142+
span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'cache.set_item',
143+
);
144+
145+
// We should have both get and set operations
146+
expect(allGetItemSpans?.length).toBeGreaterThan(0);
147+
expect(allSetItemSpans?.length).toBeGreaterThan(0);
148+
149+
// Check for cache misses (cache.hit = false)
150+
const cacheMissSpans = allGetItemSpans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_HIT] === false);
151+
152+
// Check for cache hits (cache.hit = true)
153+
const cacheHitSpans = allGetItemSpans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_HIT] === true);
154+
155+
// We should have at least one cache miss (first calls to getCachedUser and getCachedData)
156+
expect(cacheMissSpans?.length).toBeGreaterThanOrEqual(1);
157+
158+
// We should have at least one cache hit (second calls to getCachedUser and getCachedData)
159+
expect(cacheHitSpans?.length).toBeGreaterThanOrEqual(1);
160+
});
161+
});
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { cachedFunction, defineCachedEventHandler, defineEventHandler, getQuery } from '#imports';
2+
3+
// Test cachedFunction
4+
const getCachedUser = cachedFunction(
5+
async (userId: string) => {
6+
return {
7+
id: userId,
8+
name: `User ${userId}`,
9+
email: `user${userId}@example.com`,
10+
timestamp: Date.now(),
11+
};
12+
},
13+
{
14+
maxAge: 60,
15+
name: 'getCachedUser',
16+
getKey: (userId: string) => `user:${userId}`,
17+
},
18+
);
19+
20+
// Test cachedFunction with different options
21+
const getCachedData = cachedFunction(
22+
async (key: string) => {
23+
return {
24+
key,
25+
value: `cached-value-${key}`,
26+
timestamp: Date.now(),
27+
};
28+
},
29+
{
30+
maxAge: 120,
31+
name: 'getCachedData',
32+
getKey: (key: string) => `data:${key}`,
33+
},
34+
);
35+
36+
// Test defineCachedEventHandler
37+
const cachedHandler = defineCachedEventHandler(
38+
async event => {
39+
return {
40+
message: 'This response is cached',
41+
timestamp: Date.now(),
42+
path: event.path,
43+
};
44+
},
45+
{
46+
maxAge: 60,
47+
name: 'cachedHandler',
48+
},
49+
);
50+
51+
export default defineEventHandler(async event => {
52+
const results: Record<string, unknown> = {};
53+
const testKey = String(getQuery(event).user ?? '123');
54+
const dataKey = String(getQuery(event).data ?? 'test-key');
55+
56+
// Test cachedFunction - first call (cache miss)
57+
const user1 = await getCachedUser(testKey);
58+
results.cachedUser1 = user1;
59+
60+
// Test cachedFunction - second call (cache hit)
61+
const user2 = await getCachedUser(testKey);
62+
results.cachedUser2 = user2;
63+
64+
// Test cachedFunction with different key (cache miss)
65+
const user3 = await getCachedUser(`${testKey}456`);
66+
results.cachedUser3 = user3;
67+
68+
// Test another cachedFunction
69+
const data1 = await getCachedData(dataKey);
70+
results.cachedData1 = data1;
71+
72+
// Test cachedFunction - cache hit
73+
const data2 = await getCachedData(dataKey);
74+
results.cachedData2 = data2;
75+
76+
// Test cachedEventHandler by calling it
77+
const cachedResponse = await cachedHandler(event);
78+
results.cachedResponse = cachedResponse;
79+
80+
return {
81+
success: true,
82+
results,
83+
};
84+
});

0 commit comments

Comments
 (0)