Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 55 additions & 0 deletions dev-packages/cloudflare-integration-tests/suites/kv/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { KVNamespace } from '@cloudflare/workers-types';

interface Env {
MY_KV: KVNamespace;
}

export default {
async fetch(request, env) {
const url = new URL(request.url);

if (url.pathname === '/kv/get') {
await env.MY_KV.put('get-key', 'get-value');
const value = await env.MY_KV.get('get-key');
return new Response(value);
}

if (url.pathname === '/kv/get-miss') {
const value = await env.MY_KV.get('missing-key');
return new Response(value ?? 'miss');
}

if (url.pathname === '/kv/bulk-get') {
await env.MY_KV.put('bulk-a', 'a');
await env.MY_KV.put('bulk-b', 'b');
const values = await env.MY_KV.get(['bulk-a', 'bulk-b']);
return Response.json(Object.fromEntries(values));
}

if (url.pathname === '/kv/get-with-metadata') {
await env.MY_KV.put('metadata-key', 'metadata-value', { metadata: { source: 'test' } });
const value = await env.MY_KV.getWithMetadata('metadata-key');
return Response.json(value);
}

if (url.pathname === '/kv/put') {
await env.MY_KV.put('put-key', 'put-value');
return new Response('OK');
}

if (url.pathname === '/kv/list') {
await env.MY_KV.put('list-a', 'a');
await env.MY_KV.put('list-b', 'b');
await env.MY_KV.list({ prefix: 'list-' });
return new Response('OK');
}

if (url.pathname === '/kv/delete') {
await env.MY_KV.put('delete-key', 'gone');
await env.MY_KV.delete('delete-key');
return new Response('OK');
}

return new Response('not found', { status: 404 });
},
} satisfies ExportedHandler<Env>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineCloudflareOptions } from '@sentry/cloudflare';

export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
}));
290 changes: 290 additions & 0 deletions dev-packages/cloudflare-integration-tests/suites/kv/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
import {
CACHE_HIT,
CACHE_KEY,
CACHE_OPERATION,
DB_COLLECTION_NAME,
DB_OPERATION_NAME,
DB_SYSTEM_NAME,
SENTRY_ENVIRONMENT,
SENTRY_IS_LOCALHOST,
SENTRY_OP,
SENTRY_ORIGIN,
SENTRY_SDK_NAME,
SENTRY_SDK_VERSION,
SENTRY_SEGMENT_ID,
SENTRY_SEGMENT_NAME,
SENTRY_TRACE_LIFECYCLE,
URL_PATH,
} from '@sentry/conventions/attributes';
import { CACHE_GET, CACHE_PUT, CACHE_REMOVE } from '@sentry/conventions/op';
import { CACHE_OPERATION_NAMES, SDK_VERSION } from '@sentry/core';
import { expect, it } from 'vitest';
import { createRunner } from '../../runner';
import { getSpansFromEnvelope } from '../../spanUtils';

it('emits cache.get for a KV read with key and hit attributes', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const spans = getSpansFromEnvelope(envelope);
const segmentSpan = spans.find(span => span.is_segment);
const getSpans = spans.filter(span => span.name === CACHE_GET);

expect(getSpans).toHaveLength(1);
expect(getSpans[0]?.attributes).toEqual({
[SENTRY_OP]: { type: 'string', value: CACHE_GET },
[SENTRY_ORIGIN]: { type: 'string', value: 'auto.cache.cloudflare.kv' },
[CACHE_OPERATION]: { type: 'string', value: CACHE_OPERATION_NAMES[CACHE_GET] },
[CACHE_KEY]: { type: 'array', value: ['get-key'] },
[CACHE_HIT]: { type: 'boolean', value: true },
[DB_OPERATION_NAME]: { type: 'string', value: 'get' },
[DB_COLLECTION_NAME]: { type: 'string', value: 'MY_KV' },
[DB_SYSTEM_NAME]: { type: 'string', value: 'cloudflare-kv' },
[SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' },
[SENTRY_SEGMENT_NAME]: { type: 'string', value: 'GET' },
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpan?.span_id },
[SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.cloudflare' },
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
[SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
[SENTRY_IS_LOCALHOST]: { type: 'boolean', value: true },
});
})
.start(signal);

await runner.makeRequest('get', '/kv/get');
await runner.completed();
});

it('emits cache.get with cache.hit false for a KV read of a missing key', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const spans = getSpansFromEnvelope(envelope);
const segmentSpan = spans.find(span => span.is_segment);
const getSpans = spans.filter(span => span.name === CACHE_GET);

expect(getSpans).toHaveLength(1);
expect(getSpans[0]?.attributes).toEqual({
[SENTRY_OP]: { type: 'string', value: CACHE_GET },
[SENTRY_ORIGIN]: { type: 'string', value: 'auto.cache.cloudflare.kv' },
[CACHE_OPERATION]: { type: 'string', value: CACHE_OPERATION_NAMES[CACHE_GET] },
[CACHE_KEY]: { type: 'array', value: ['missing-key'] },
[CACHE_HIT]: { type: 'boolean', value: false },
[DB_OPERATION_NAME]: { type: 'string', value: 'get' },
[DB_COLLECTION_NAME]: { type: 'string', value: 'MY_KV' },
[DB_SYSTEM_NAME]: { type: 'string', value: 'cloudflare-kv' },
[SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' },
[SENTRY_SEGMENT_NAME]: { type: 'string', value: 'GET' },
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpan?.span_id },
[SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.cloudflare' },
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
[SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
[SENTRY_IS_LOCALHOST]: { type: 'boolean', value: true },
});
})
.start(signal);

await runner.makeRequest('get', '/kv/get-miss');
await runner.completed();
});

it('emits cache.get for a bulk KV read with all keys in cache.key', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const spans = getSpansFromEnvelope(envelope);
const segmentSpan = spans.find(span => span.is_segment);
const getSpans = spans.filter(span => span.name === CACHE_GET);

expect(getSpans).toHaveLength(1);
expect(getSpans[0]?.attributes).toEqual({
[SENTRY_OP]: { type: 'string', value: CACHE_GET },
[SENTRY_ORIGIN]: { type: 'string', value: 'auto.cache.cloudflare.kv' },
[CACHE_OPERATION]: { type: 'string', value: CACHE_OPERATION_NAMES[CACHE_GET] },
[CACHE_KEY]: { type: 'array', value: ['bulk-a', 'bulk-b'] },
[CACHE_HIT]: { type: 'boolean', value: true },
[DB_OPERATION_NAME]: { type: 'string', value: 'get' },
[DB_COLLECTION_NAME]: { type: 'string', value: 'MY_KV' },
[DB_SYSTEM_NAME]: { type: 'string', value: 'cloudflare-kv' },
[SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' },
[SENTRY_SEGMENT_NAME]: { type: 'string', value: 'GET' },
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpan?.span_id },
[SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.cloudflare' },
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
[SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
[SENTRY_IS_LOCALHOST]: { type: 'boolean', value: true },
});
})
.start(signal);

await runner.makeRequest('get', '/kv/bulk-get');
await runner.completed();
});
Comment thread
cursor[bot] marked this conversation as resolved.

it('emits cache.get for getWithMetadata', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const spans = getSpansFromEnvelope(envelope);
const segmentSpan = spans.find(span => span.is_segment);
const getSpans = spans.filter(span => span.name === CACHE_GET);

expect(getSpans).toHaveLength(1);
expect(getSpans[0]?.attributes).toEqual({
[SENTRY_OP]: { type: 'string', value: CACHE_GET },
[SENTRY_ORIGIN]: { type: 'string', value: 'auto.cache.cloudflare.kv' },
[CACHE_OPERATION]: { type: 'string', value: CACHE_OPERATION_NAMES[CACHE_GET] },
[CACHE_KEY]: { type: 'array', value: ['metadata-key'] },
[CACHE_HIT]: { type: 'boolean', value: true },
[DB_OPERATION_NAME]: { type: 'string', value: 'getWithMetadata' },
[DB_COLLECTION_NAME]: { type: 'string', value: 'MY_KV' },
[DB_SYSTEM_NAME]: { type: 'string', value: 'cloudflare-kv' },
[SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' },
[SENTRY_SEGMENT_NAME]: { type: 'string', value: 'GET' },
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpan?.span_id },
[SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.cloudflare' },
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
[SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
[SENTRY_IS_LOCALHOST]: { type: 'boolean', value: true },
});
})
.start(signal);

await runner.makeRequest('get', '/kv/get-with-metadata');
await runner.completed();
});

it('emits cache.put for KV writes', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const spans = getSpansFromEnvelope(envelope);
const segmentSpan = spans.find(span => span.is_segment);
const putSpans = spans.filter(span => span.name === CACHE_PUT);

expect(putSpans).toHaveLength(1);
expect(putSpans[0]?.attributes).toEqual({
[SENTRY_OP]: { type: 'string', value: CACHE_PUT },
[SENTRY_ORIGIN]: { type: 'string', value: 'auto.cache.cloudflare.kv' },
[CACHE_OPERATION]: { type: 'string', value: CACHE_OPERATION_NAMES[CACHE_PUT] },
[CACHE_KEY]: { type: 'array', value: ['put-key'] },
[DB_OPERATION_NAME]: { type: 'string', value: 'put' },
[DB_COLLECTION_NAME]: { type: 'string', value: 'MY_KV' },
[DB_SYSTEM_NAME]: { type: 'string', value: 'cloudflare-kv' },
[SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' },
[SENTRY_SEGMENT_NAME]: { type: 'string', value: 'GET' },
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpan?.span_id },
[SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.cloudflare' },
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
[SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
[SENTRY_IS_LOCALHOST]: { type: 'boolean', value: true },
});
})
.start(signal);

await runner.makeRequest('get', '/kv/put');
await runner.completed();
});

it('emits cache.remove for KV deletes', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const spans = getSpansFromEnvelope(envelope);
const segmentSpan = spans.find(span => span.is_segment);
const removeSpans = spans.filter(span => span.name === CACHE_REMOVE);

expect(removeSpans).toHaveLength(1);
expect(removeSpans[0]?.attributes).toEqual({
[SENTRY_OP]: { type: 'string', value: CACHE_REMOVE },
[SENTRY_ORIGIN]: { type: 'string', value: 'auto.cache.cloudflare.kv' },
[CACHE_OPERATION]: { type: 'string', value: CACHE_OPERATION_NAMES[CACHE_REMOVE] },
[CACHE_KEY]: { type: 'array', value: ['delete-key'] },
[DB_OPERATION_NAME]: { type: 'string', value: 'delete' },
[DB_COLLECTION_NAME]: { type: 'string', value: 'MY_KV' },
[DB_SYSTEM_NAME]: { type: 'string', value: 'cloudflare-kv' },
[SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' },
[SENTRY_SEGMENT_NAME]: { type: 'string', value: 'GET' },
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpan?.span_id },
[SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.cloudflare' },
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
[SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
[SENTRY_IS_LOCALHOST]: { type: 'boolean', value: true },
});
})
.start(signal);

await runner.makeRequest('get', '/kv/delete');
await runner.completed();
});

it('emits cache.get without cache.key or cache.hit for KV list operations', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const spans = getSpansFromEnvelope(envelope);
const segmentSpan = spans.find(span => span.is_segment);
const getSpans = spans.filter(span => span.name === CACHE_GET);

expect(getSpans).toHaveLength(1);
expect(getSpans[0]?.attributes).toEqual({
[SENTRY_OP]: { type: 'string', value: CACHE_GET },
[SENTRY_ORIGIN]: { type: 'string', value: 'auto.cache.cloudflare.kv' },
[CACHE_OPERATION]: { type: 'string', value: CACHE_OPERATION_NAMES[CACHE_GET] },
[DB_OPERATION_NAME]: { type: 'string', value: 'list' },
[DB_COLLECTION_NAME]: { type: 'string', value: 'MY_KV' },
[DB_SYSTEM_NAME]: { type: 'string', value: 'cloudflare-kv' },
[SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' },
[SENTRY_SEGMENT_NAME]: { type: 'string', value: 'GET' },
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpan?.span_id },
[SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.cloudflare' },
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
[SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
[SENTRY_IS_LOCALHOST]: { type: 'boolean', value: true },
});
})
.start(signal);

await runner.makeRequest('get', '/kv/list');
await runner.completed();
});
Comment thread
cursor[bot] marked this conversation as resolved.

it("emits the KV spans of each request under that request's own segment span", async ({ signal }) => {
const segmentTraceIds: string[] = [];
const runner = createRunner(__dirname)
.unordered()
.expect(envelope => {
const spans = getSpansFromEnvelope(envelope);
const segmentSpan = spans.find(span => span.is_segment);

expect(segmentSpan?.attributes[URL_PATH]).toEqual({ type: 'string', value: '/kv/get' });
expect(spans.filter(span => !span.is_segment)).toEqual([
expect.objectContaining({
name: CACHE_PUT,
trace_id: segmentSpan?.trace_id,
parent_span_id: segmentSpan?.span_id,
}),
expect.objectContaining({
name: CACHE_GET,
trace_id: segmentSpan?.trace_id,
parent_span_id: segmentSpan?.span_id,
}),
]);
segmentTraceIds.push(segmentSpan!.trace_id);
})
.expect(envelope => {
const spans = getSpansFromEnvelope(envelope);
const segmentSpan = spans.find(span => span.is_segment);

expect(segmentSpan?.attributes[URL_PATH]).toEqual({ type: 'string', value: '/kv/put' });
expect(spans.filter(span => !span.is_segment)).toEqual([
expect.objectContaining({
name: CACHE_PUT,
trace_id: segmentSpan?.trace_id,
parent_span_id: segmentSpan?.span_id,
}),
]);
segmentTraceIds.push(segmentSpan!.trace_id);
})
.start(signal);

await runner.makeRequest('get', '/kv/get');
await runner.makeRequest('get', '/kv/put');
await runner.completed();

expect(segmentTraceIds).toHaveLength(2);
expect(segmentTraceIds[0]).not.toBe(segmentTraceIds[1]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { cloudflare } from '@cloudflare/vite-plugin';
import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [cloudflare(), sentryCloudflareVitePlugin()],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "../../node_modules/wrangler/config-schema.json",
"name": "cloudflare-kv",
"main": "index.ts",
"compatibility_date": "2025-06-17",
"compatibility_flags": ["nodejs_compat"],
"kv_namespaces": [
{
"binding": "MY_KV",
"id": "00000000000000000000000000000000",
},
],
}
Loading
Loading