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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1041,7 +1041,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Deno
if:
matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed' || matrix.test-application ==
matrix.test-application == 'deno' || matrix.test-application == 'deno-static' || matrix.test-application ==
'deno-redis' || matrix.test-application == 'hono-4' || matrix.test-application == 'deno-mysql' ||
matrix.test-application == 'deno-pg'
uses: denoland/setup-deno@v2.0.5
Expand DownExpand Up@@ -1166,7 +1166,7 @@ jobs:
with:
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Set up Deno
if: matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed'
if: matrix.test-application == 'deno'
uses: denoland/setup-deno@v2.0.5
with:
deno-version: ${{ matrix.deno-version || 'v2.8.3' }}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
{
"name": "deno-streamed-app",
"name": "deno-static-app",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "deno run --allow-net --allow-env --allow-read --allow-sys src/app.ts",
"start": "deno run --allow-net --allow-env --allow-read src/app.ts",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api": "^1.9.0",
"ai": "^3.0.0",
"zod": "^3.22.4"
},
Expand All@@ -21,13 +21,5 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"optionalVariants": [
{
"deno-version": "latest",
"label": "deno-streamed (latest)"
}
]
}
}
307 changes: 307 additions & 0 deletions dev-packages/e2e-tests/test-applications/deno-static/src/app.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
import { trace } from '@opentelemetry/api';

// Simulate a pre-existing OTel provider (like Supabase Edge Runtime registers
// before user code runs). Without trace.disable() in Sentry's setup, this would
// cause setGlobalTracerProvider to be a no-op, silently dropping all OTel spans.
const fakeProvider = {
getTracer: () => ({
startSpan: () => ({ end: () => {}, setAttributes: () => {} }),
startActiveSpan: (_name: string, fn: Function) => fn({ end: () => {}, setAttributes: () => {} }),
}),
};
trace.setGlobalTracerProvider(fakeProvider as any);

// Sentry.init() must call trace.disable() to clear the fake provider above
import * as Sentry from '@sentry/deno';
import { generateText } from 'ai';
import { MockLanguageModelV1 } from 'ai/test';
import { z } from 'zod';

Sentry.init({
traceLifecycle: 'static',
environment: 'qa',
dsn: Deno.env.get('E2E_TEST_DSN'),
debug: !!Deno.env.get('DEBUG'),
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1,
});

const port = 3030;

Deno.serve({ port }, async (req: Request) => {
const url = new URL(req.url);

if (url.pathname === '/test-success') {
return new Response(JSON.stringify({ version: 'v1' }), {
headers: { 'Content-Type': 'application/json' },
});
}

if (url.pathname === '/test-error') {
const exceptionId = Sentry.captureException(new Error('This is an error'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test Sentry.startSpan — uses Sentry's internal pipeline
if (url.pathname === '/test-sentry-span') {
Sentry.startSpan({ name: 'test-sentry-span' }, () => {
// noop
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startSpan — goes through the global TracerProvider
if (url.pathname === '/test-otel-span') {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('test-otel-span');
span.end();
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startActiveSpan — what AI SDK and most instrumentations use
if (url.pathname === '/test-otel-active-span') {
const tracer = trace.getTracer('test-tracer');
tracer.startActiveSpan('test-otel-active-span', span => {
span.setAttributes({ 'test.active': true });
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test interop: OTel span inside a Sentry span
if (url.pathname === '/test-interop') {
Sentry.startSpan({ name: 'sentry-parent' }, () => {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('otel-child');
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test breadcrumbs: add a breadcrumb then capture an error
if (url.pathname === '/test-breadcrumb') {
Sentry.addBreadcrumb({
message: 'test-breadcrumb',
category: 'custom',
level: 'info',
});
const exceptionId = Sentry.captureException(new Error('breadcrumb-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test context: set user, tag, extra then capture an error
if (url.pathname === '/test-context') {
Sentry.setUser({ id: '123', email: 'test@sentry.io' });
Sentry.setTag('deno-runtime', 'true');
Sentry.setExtra('detail', { key: 'value' });
const exceptionId = Sentry.captureException(new Error('context-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test scope isolation: tags inside withScope do not leak
if (url.pathname === '/test-scope-isolation') {
let insideId: string | undefined;
let outsideId: string | undefined;

Sentry.withScope(scope => {
scope.setTag('isolated', 'yes');
insideId = Sentry.captureException(new Error('inside-scope'));
});

outsideId = Sentry.captureException(new Error('outside-scope'));

return new Response(JSON.stringify({ insideId, outsideId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test outbound fetch instrumentation
if (url.pathname === '/test-outgoing-fetch') {
const response = await Sentry.startSpan({ name: 'test-outgoing-fetch' }, async () => {
const res = await fetch('http://localhost:3030/test-success');
return res.json();
});
return new Response(JSON.stringify(response), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI: Vercel AI SDK generateText with mock model
if (url.pathname === '/test-ai') {
const results = await Sentry.startSpan({ op: 'function', name: 'ai-test' }, async () => {
// First call - telemetry enabled by default
const result1 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'First span here!',
}),
}),
prompt: 'Where is the first span?',
});

// Second call - explicitly enabled telemetry
const result2 = await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Second span here!',
}),
}),
prompt: 'Where is the second span?',
});

// Third call - with tool calls
const result3 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (args: { location: string }) => {
return `Weather in ${args.location}: Sunny, 72°F`;
},
},
},
prompt: 'What is the weather in San Francisco?',
});

// Fourth call - explicitly disabled telemetry, should not be captured
const result4 = await generateText({
experimental_telemetry: { isEnabled: false },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Should not be captured!',
}),
}),
prompt: 'Where is the disabled span?',
});

return {
result1: result1.text,
result2: result2.text,
result3: result3.text,
result4: result4.text,
};
});

return new Response(JSON.stringify(results), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI error: tool call that throws
if (url.pathname === '/test-ai-error') {
try {
await Sentry.startSpan({ op: 'function', name: 'ai-error-test' }, async () => {
await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (_args: { location: string }) => {
throw new Error('Tool call failed');
},
},
},
prompt: 'What is the weather in San Francisco?',
});
});
} catch (e) {
Sentry.captureException(e);
}

return new Response(JSON.stringify({ status: 'error-handled' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test metrics: emit counter, distribution, and gauge
if (url.pathname === '/test-metrics') {
Sentry.metrics.count('test.deno.count', 1, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Apples',
},
});
Sentry.metrics.distribution('test.deno.distribution', 100, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Bananas',
},
});
Sentry.metrics.gauge('test.deno.gauge', 200, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Cherries',
},
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test logs: emit a debug log via Sentry.logger
if (url.pathname === '/test-log') {
Sentry.logger.debug('Accessed /test-log route');
return new Response(JSON.stringify({ message: 'Log sent' }), {
headers: { 'Content-Type': 'application/json' },
});
}

return new Response('Not found', { status: 404 });
});

console.log(`Deno test app listening on port ${port}`);
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'deno-streamed',
proxyServerName: 'deno-static',
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1041,7 +1041,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Deno
if:
matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed' || matrix.test-application ==
matrix.test-application == 'deno' || matrix.test-application == 'deno-static' || matrix.test-application ==
'deno-redis' || matrix.test-application == 'hono-4' || matrix.test-application == 'deno-mysql' ||
matrix.test-application == 'deno-pg'
uses: denoland/setup-deno@v2.0.5
Expand DownExpand Up@@ -1166,7 +1166,7 @@ jobs:
with:
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Set up Deno
if: matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed'
if: matrix.test-application == 'deno'
uses: denoland/setup-deno@v2.0.5
with:
deno-version: ${{ matrix.deno-version || 'v2.8.3' }}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
{
"name": "deno-streamed-app",
"name": "deno-static-app",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "deno run --allow-net --allow-env --allow-read --allow-sys src/app.ts",
"start": "deno run --allow-net --allow-env --allow-read src/app.ts",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api": "^1.9.0",
"ai": "^3.0.0",
"zod": "^3.22.4"
},
Expand All@@ -21,13 +21,5 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"optionalVariants": [
{
"deno-version": "latest",
"label": "deno-streamed (latest)"
}
]
}
}
307 changes: 307 additions & 0 deletions dev-packages/e2e-tests/test-applications/deno-static/src/app.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
import { trace } from '@opentelemetry/api';

// Simulate a pre-existing OTel provider (like Supabase Edge Runtime registers
// before user code runs). Without trace.disable() in Sentry's setup, this would
// cause setGlobalTracerProvider to be a no-op, silently dropping all OTel spans.
const fakeProvider = {
getTracer: () => ({
startSpan: () => ({ end: () => {}, setAttributes: () => {} }),
startActiveSpan: (_name: string, fn: Function) => fn({ end: () => {}, setAttributes: () => {} }),
}),
};
trace.setGlobalTracerProvider(fakeProvider as any);

// Sentry.init() must call trace.disable() to clear the fake provider above
import * as Sentry from '@sentry/deno';
import { generateText } from 'ai';
import { MockLanguageModelV1 } from 'ai/test';
import { z } from 'zod';

Sentry.init({
traceLifecycle: 'static',
environment: 'qa',
dsn: Deno.env.get('E2E_TEST_DSN'),
debug: !!Deno.env.get('DEBUG'),
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1,
});

const port = 3030;

Deno.serve({ port }, async (req: Request) => {
const url = new URL(req.url);

if (url.pathname === '/test-success') {
return new Response(JSON.stringify({ version: 'v1' }), {
headers: { 'Content-Type': 'application/json' },
});
}

if (url.pathname === '/test-error') {
const exceptionId = Sentry.captureException(new Error('This is an error'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test Sentry.startSpan — uses Sentry's internal pipeline
if (url.pathname === '/test-sentry-span') {
Sentry.startSpan({ name: 'test-sentry-span' }, () => {
// noop
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startSpan — goes through the global TracerProvider
if (url.pathname === '/test-otel-span') {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('test-otel-span');
span.end();
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startActiveSpan — what AI SDK and most instrumentations use
if (url.pathname === '/test-otel-active-span') {
const tracer = trace.getTracer('test-tracer');
tracer.startActiveSpan('test-otel-active-span', span => {
span.setAttributes({ 'test.active': true });
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test interop: OTel span inside a Sentry span
if (url.pathname === '/test-interop') {
Sentry.startSpan({ name: 'sentry-parent' }, () => {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('otel-child');
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test breadcrumbs: add a breadcrumb then capture an error
if (url.pathname === '/test-breadcrumb') {
Sentry.addBreadcrumb({
message: 'test-breadcrumb',
category: 'custom',
level: 'info',
});
const exceptionId = Sentry.captureException(new Error('breadcrumb-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test context: set user, tag, extra then capture an error
if (url.pathname === '/test-context') {
Sentry.setUser({ id: '123', email: 'test@sentry.io' });
Sentry.setTag('deno-runtime', 'true');
Sentry.setExtra('detail', { key: 'value' });
const exceptionId = Sentry.captureException(new Error('context-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test scope isolation: tags inside withScope do not leak
if (url.pathname === '/test-scope-isolation') {
let insideId: string | undefined;
let outsideId: string | undefined;

Sentry.withScope(scope => {
scope.setTag('isolated', 'yes');
insideId = Sentry.captureException(new Error('inside-scope'));
});

outsideId = Sentry.captureException(new Error('outside-scope'));

return new Response(JSON.stringify({ insideId, outsideId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test outbound fetch instrumentation
if (url.pathname === '/test-outgoing-fetch') {
const response = await Sentry.startSpan({ name: 'test-outgoing-fetch' }, async () => {
const res = await fetch('http://localhost:3030/test-success');
return res.json();
});
return new Response(JSON.stringify(response), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI: Vercel AI SDK generateText with mock model
if (url.pathname === '/test-ai') {
const results = await Sentry.startSpan({ op: 'function', name: 'ai-test' }, async () => {
// First call - telemetry enabled by default
const result1 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'First span here!',
}),
}),
prompt: 'Where is the first span?',
});

// Second call - explicitly enabled telemetry
const result2 = await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Second span here!',
}),
}),
prompt: 'Where is the second span?',
});

// Third call - with tool calls
const result3 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (args: { location: string }) => {
return `Weather in ${args.location}: Sunny, 72°F`;
},
},
},
prompt: 'What is the weather in San Francisco?',
});

// Fourth call - explicitly disabled telemetry, should not be captured
const result4 = await generateText({
experimental_telemetry: { isEnabled: false },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Should not be captured!',
}),
}),
prompt: 'Where is the disabled span?',
});

return {
result1: result1.text,
result2: result2.text,
result3: result3.text,
result4: result4.text,
};
});

return new Response(JSON.stringify(results), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI error: tool call that throws
if (url.pathname === '/test-ai-error') {
try {
await Sentry.startSpan({ op: 'function', name: 'ai-error-test' }, async () => {
await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (_args: { location: string }) => {
throw new Error('Tool call failed');
},
},
},
prompt: 'What is the weather in San Francisco?',
});
});
} catch (e) {
Sentry.captureException(e);
}

return new Response(JSON.stringify({ status: 'error-handled' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test metrics: emit counter, distribution, and gauge
if (url.pathname === '/test-metrics') {
Sentry.metrics.count('test.deno.count', 1, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Apples',
},
});
Sentry.metrics.distribution('test.deno.distribution', 100, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Bananas',
},
});
Sentry.metrics.gauge('test.deno.gauge', 200, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Cherries',
},
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test logs: emit a debug log via Sentry.logger
if (url.pathname === '/test-log') {
Sentry.logger.debug('Accessed /test-log route');
return new Response(JSON.stringify({ message: 'Log sent' }), {
headers: { 'Content-Type': 'application/json' },
});
}

return new Response('Not found', { status: 404 });
});

console.log(`Deno test app listening on port ${port}`);
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'deno-streamed',
proxyServerName: 'deno-static',
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1041,7 +1041,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Deno
if:
matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed' || matrix.test-application ==
matrix.test-application == 'deno' || matrix.test-application == 'deno-static' || matrix.test-application ==
'deno-redis' || matrix.test-application == 'hono-4' || matrix.test-application == 'deno-mysql' ||
matrix.test-application == 'deno-pg'
uses: denoland/setup-deno@v2.0.5
Expand DownExpand Up@@ -1166,7 +1166,7 @@ jobs:
with:
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Set up Deno
if: matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed'
if: matrix.test-application == 'deno'
uses: denoland/setup-deno@v2.0.5
with:
deno-version: ${{ matrix.deno-version || 'v2.8.3' }}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
{
"name": "deno-streamed-app",
"name": "deno-static-app",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "deno run --allow-net --allow-env --allow-read --allow-sys src/app.ts",
"start": "deno run --allow-net --allow-env --allow-read src/app.ts",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api": "^1.9.0",
"ai": "^3.0.0",
"zod": "^3.22.4"
},
Expand All@@ -21,13 +21,5 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"optionalVariants": [
{
"deno-version": "latest",
"label": "deno-streamed (latest)"
}
]
}
}
307 changes: 307 additions & 0 deletions dev-packages/e2e-tests/test-applications/deno-static/src/app.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
import { trace } from '@opentelemetry/api';

// Simulate a pre-existing OTel provider (like Supabase Edge Runtime registers
// before user code runs). Without trace.disable() in Sentry's setup, this would
// cause setGlobalTracerProvider to be a no-op, silently dropping all OTel spans.
const fakeProvider = {
getTracer: () => ({
startSpan: () => ({ end: () => {}, setAttributes: () => {} }),
startActiveSpan: (_name: string, fn: Function) => fn({ end: () => {}, setAttributes: () => {} }),
}),
};
trace.setGlobalTracerProvider(fakeProvider as any);

// Sentry.init() must call trace.disable() to clear the fake provider above
import * as Sentry from '@sentry/deno';
import { generateText } from 'ai';
import { MockLanguageModelV1 } from 'ai/test';
import { z } from 'zod';

Sentry.init({
traceLifecycle: 'static',
environment: 'qa',
dsn: Deno.env.get('E2E_TEST_DSN'),
debug: !!Deno.env.get('DEBUG'),
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1,
});

const port = 3030;

Deno.serve({ port }, async (req: Request) => {
const url = new URL(req.url);

if (url.pathname === '/test-success') {
return new Response(JSON.stringify({ version: 'v1' }), {
headers: { 'Content-Type': 'application/json' },
});
}

if (url.pathname === '/test-error') {
const exceptionId = Sentry.captureException(new Error('This is an error'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test Sentry.startSpan — uses Sentry's internal pipeline
if (url.pathname === '/test-sentry-span') {
Sentry.startSpan({ name: 'test-sentry-span' }, () => {
// noop
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startSpan — goes through the global TracerProvider
if (url.pathname === '/test-otel-span') {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('test-otel-span');
span.end();
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startActiveSpan — what AI SDK and most instrumentations use
if (url.pathname === '/test-otel-active-span') {
const tracer = trace.getTracer('test-tracer');
tracer.startActiveSpan('test-otel-active-span', span => {
span.setAttributes({ 'test.active': true });
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test interop: OTel span inside a Sentry span
if (url.pathname === '/test-interop') {
Sentry.startSpan({ name: 'sentry-parent' }, () => {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('otel-child');
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test breadcrumbs: add a breadcrumb then capture an error
if (url.pathname === '/test-breadcrumb') {
Sentry.addBreadcrumb({
message: 'test-breadcrumb',
category: 'custom',
level: 'info',
});
const exceptionId = Sentry.captureException(new Error('breadcrumb-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test context: set user, tag, extra then capture an error
if (url.pathname === '/test-context') {
Sentry.setUser({ id: '123', email: 'test@sentry.io' });
Sentry.setTag('deno-runtime', 'true');
Sentry.setExtra('detail', { key: 'value' });
const exceptionId = Sentry.captureException(new Error('context-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test scope isolation: tags inside withScope do not leak
if (url.pathname === '/test-scope-isolation') {
let insideId: string | undefined;
let outsideId: string | undefined;

Sentry.withScope(scope => {
scope.setTag('isolated', 'yes');
insideId = Sentry.captureException(new Error('inside-scope'));
});

outsideId = Sentry.captureException(new Error('outside-scope'));

return new Response(JSON.stringify({ insideId, outsideId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test outbound fetch instrumentation
if (url.pathname === '/test-outgoing-fetch') {
const response = await Sentry.startSpan({ name: 'test-outgoing-fetch' }, async () => {
const res = await fetch('http://localhost:3030/test-success');
return res.json();
});
return new Response(JSON.stringify(response), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI: Vercel AI SDK generateText with mock model
if (url.pathname === '/test-ai') {
const results = await Sentry.startSpan({ op: 'function', name: 'ai-test' }, async () => {
// First call - telemetry enabled by default
const result1 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'First span here!',
}),
}),
prompt: 'Where is the first span?',
});

// Second call - explicitly enabled telemetry
const result2 = await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Second span here!',
}),
}),
prompt: 'Where is the second span?',
});

// Third call - with tool calls
const result3 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (args: { location: string }) => {
return `Weather in ${args.location}: Sunny, 72°F`;
},
},
},
prompt: 'What is the weather in San Francisco?',
});

// Fourth call - explicitly disabled telemetry, should not be captured
const result4 = await generateText({
experimental_telemetry: { isEnabled: false },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Should not be captured!',
}),
}),
prompt: 'Where is the disabled span?',
});

return {
result1: result1.text,
result2: result2.text,
result3: result3.text,
result4: result4.text,
};
});

return new Response(JSON.stringify(results), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI error: tool call that throws
if (url.pathname === '/test-ai-error') {
try {
await Sentry.startSpan({ op: 'function', name: 'ai-error-test' }, async () => {
await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (_args: { location: string }) => {
throw new Error('Tool call failed');
},
},
},
prompt: 'What is the weather in San Francisco?',
});
});
} catch (e) {
Sentry.captureException(e);
}

return new Response(JSON.stringify({ status: 'error-handled' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test metrics: emit counter, distribution, and gauge
if (url.pathname === '/test-metrics') {
Sentry.metrics.count('test.deno.count', 1, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Apples',
},
});
Sentry.metrics.distribution('test.deno.distribution', 100, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Bananas',
},
});
Sentry.metrics.gauge('test.deno.gauge', 200, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Cherries',
},
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test logs: emit a debug log via Sentry.logger
if (url.pathname === '/test-log') {
Sentry.logger.debug('Accessed /test-log route');
return new Response(JSON.stringify({ message: 'Log sent' }), {
headers: { 'Content-Type': 'application/json' },
});
}

return new Response('Not found', { status: 404 });
});

console.log(`Deno test app listening on port ${port}`);
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'deno-streamed',
proxyServerName: 'deno-static',
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1041,7 +1041,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Deno
if:
matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed' || matrix.test-application ==
matrix.test-application == 'deno' || matrix.test-application == 'deno-static' || matrix.test-application ==
'deno-redis' || matrix.test-application == 'hono-4' || matrix.test-application == 'deno-mysql' ||
matrix.test-application == 'deno-pg'
uses: denoland/setup-deno@v2.0.5
Expand DownExpand Up@@ -1166,7 +1166,7 @@ jobs:
with:
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Set up Deno
if: matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed'
if: matrix.test-application == 'deno'
uses: denoland/setup-deno@v2.0.5
with:
deno-version: ${{ matrix.deno-version || 'v2.8.3' }}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
{
"name": "deno-streamed-app",
"name": "deno-static-app",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "deno run --allow-net --allow-env --allow-read --allow-sys src/app.ts",
"start": "deno run --allow-net --allow-env --allow-read src/app.ts",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api": "^1.9.0",
"ai": "^3.0.0",
"zod": "^3.22.4"
},
Expand All@@ -21,13 +21,5 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"optionalVariants": [
{
"deno-version": "latest",
"label": "deno-streamed (latest)"
}
]
}
}
307 changes: 307 additions & 0 deletions dev-packages/e2e-tests/test-applications/deno-static/src/app.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
import { trace } from '@opentelemetry/api';

// Simulate a pre-existing OTel provider (like Supabase Edge Runtime registers
// before user code runs). Without trace.disable() in Sentry's setup, this would
// cause setGlobalTracerProvider to be a no-op, silently dropping all OTel spans.
const fakeProvider = {
getTracer: () => ({
startSpan: () => ({ end: () => {}, setAttributes: () => {} }),
startActiveSpan: (_name: string, fn: Function) => fn({ end: () => {}, setAttributes: () => {} }),
}),
};
trace.setGlobalTracerProvider(fakeProvider as any);

// Sentry.init() must call trace.disable() to clear the fake provider above
import * as Sentry from '@sentry/deno';
import { generateText } from 'ai';
import { MockLanguageModelV1 } from 'ai/test';
import { z } from 'zod';

Sentry.init({
traceLifecycle: 'static',
environment: 'qa',
dsn: Deno.env.get('E2E_TEST_DSN'),
debug: !!Deno.env.get('DEBUG'),
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1,
});

const port = 3030;

Deno.serve({ port }, async (req: Request) => {
const url = new URL(req.url);

if (url.pathname === '/test-success') {
return new Response(JSON.stringify({ version: 'v1' }), {
headers: { 'Content-Type': 'application/json' },
});
}

if (url.pathname === '/test-error') {
const exceptionId = Sentry.captureException(new Error('This is an error'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test Sentry.startSpan — uses Sentry's internal pipeline
if (url.pathname === '/test-sentry-span') {
Sentry.startSpan({ name: 'test-sentry-span' }, () => {
// noop
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startSpan — goes through the global TracerProvider
if (url.pathname === '/test-otel-span') {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('test-otel-span');
span.end();
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startActiveSpan — what AI SDK and most instrumentations use
if (url.pathname === '/test-otel-active-span') {
const tracer = trace.getTracer('test-tracer');
tracer.startActiveSpan('test-otel-active-span', span => {
span.setAttributes({ 'test.active': true });
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test interop: OTel span inside a Sentry span
if (url.pathname === '/test-interop') {
Sentry.startSpan({ name: 'sentry-parent' }, () => {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('otel-child');
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test breadcrumbs: add a breadcrumb then capture an error
if (url.pathname === '/test-breadcrumb') {
Sentry.addBreadcrumb({
message: 'test-breadcrumb',
category: 'custom',
level: 'info',
});
const exceptionId = Sentry.captureException(new Error('breadcrumb-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test context: set user, tag, extra then capture an error
if (url.pathname === '/test-context') {
Sentry.setUser({ id: '123', email: 'test@sentry.io' });
Sentry.setTag('deno-runtime', 'true');
Sentry.setExtra('detail', { key: 'value' });
const exceptionId = Sentry.captureException(new Error('context-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test scope isolation: tags inside withScope do not leak
if (url.pathname === '/test-scope-isolation') {
let insideId: string | undefined;
let outsideId: string | undefined;

Sentry.withScope(scope => {
scope.setTag('isolated', 'yes');
insideId = Sentry.captureException(new Error('inside-scope'));
});

outsideId = Sentry.captureException(new Error('outside-scope'));

return new Response(JSON.stringify({ insideId, outsideId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test outbound fetch instrumentation
if (url.pathname === '/test-outgoing-fetch') {
const response = await Sentry.startSpan({ name: 'test-outgoing-fetch' }, async () => {
const res = await fetch('http://localhost:3030/test-success');
return res.json();
});
return new Response(JSON.stringify(response), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI: Vercel AI SDK generateText with mock model
if (url.pathname === '/test-ai') {
const results = await Sentry.startSpan({ op: 'function', name: 'ai-test' }, async () => {
// First call - telemetry enabled by default
const result1 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'First span here!',
}),
}),
prompt: 'Where is the first span?',
});

// Second call - explicitly enabled telemetry
const result2 = await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Second span here!',
}),
}),
prompt: 'Where is the second span?',
});

// Third call - with tool calls
const result3 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (args: { location: string }) => {
return `Weather in ${args.location}: Sunny, 72°F`;
},
},
},
prompt: 'What is the weather in San Francisco?',
});

// Fourth call - explicitly disabled telemetry, should not be captured
const result4 = await generateText({
experimental_telemetry: { isEnabled: false },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Should not be captured!',
}),
}),
prompt: 'Where is the disabled span?',
});

return {
result1: result1.text,
result2: result2.text,
result3: result3.text,
result4: result4.text,
};
});

return new Response(JSON.stringify(results), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI error: tool call that throws
if (url.pathname === '/test-ai-error') {
try {
await Sentry.startSpan({ op: 'function', name: 'ai-error-test' }, async () => {
await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (_args: { location: string }) => {
throw new Error('Tool call failed');
},
},
},
prompt: 'What is the weather in San Francisco?',
});
});
} catch (e) {
Sentry.captureException(e);
}

return new Response(JSON.stringify({ status: 'error-handled' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test metrics: emit counter, distribution, and gauge
if (url.pathname === '/test-metrics') {
Sentry.metrics.count('test.deno.count', 1, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Apples',
},
});
Sentry.metrics.distribution('test.deno.distribution', 100, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Bananas',
},
});
Sentry.metrics.gauge('test.deno.gauge', 200, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Cherries',
},
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test logs: emit a debug log via Sentry.logger
if (url.pathname === '/test-log') {
Sentry.logger.debug('Accessed /test-log route');
return new Response(JSON.stringify({ message: 'Log sent' }), {
headers: { 'Content-Type': 'application/json' },
});
}

return new Response('Not found', { status: 404 });
});

console.log(`Deno test app listening on port ${port}`);
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'deno-streamed',
proxyServerName: 'deno-static',
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1041,7 +1041,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Deno
if:
matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed' || matrix.test-application ==
matrix.test-application == 'deno' || matrix.test-application == 'deno-static' || matrix.test-application ==
'deno-redis' || matrix.test-application == 'hono-4' || matrix.test-application == 'deno-mysql' ||
matrix.test-application == 'deno-pg'
uses: denoland/setup-deno@v2.0.5
Expand DownExpand Up@@ -1166,7 +1166,7 @@ jobs:
with:
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Set up Deno
if: matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed'
if: matrix.test-application == 'deno'
uses: denoland/setup-deno@v2.0.5
with:
deno-version: ${{ matrix.deno-version || 'v2.8.3' }}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
{
"name": "deno-streamed-app",
"name": "deno-static-app",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "deno run --allow-net --allow-env --allow-read --allow-sys src/app.ts",
"start": "deno run --allow-net --allow-env --allow-read src/app.ts",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api": "^1.9.0",
"ai": "^3.0.0",
"zod": "^3.22.4"
},
Expand All@@ -21,13 +21,5 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"optionalVariants": [
{
"deno-version": "latest",
"label": "deno-streamed (latest)"
}
]
}
}
307 changes: 307 additions & 0 deletions dev-packages/e2e-tests/test-applications/deno-static/src/app.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
import { trace } from '@opentelemetry/api';

// Simulate a pre-existing OTel provider (like Supabase Edge Runtime registers
// before user code runs). Without trace.disable() in Sentry's setup, this would
// cause setGlobalTracerProvider to be a no-op, silently dropping all OTel spans.
const fakeProvider = {
getTracer: () => ({
startSpan: () => ({ end: () => {}, setAttributes: () => {} }),
startActiveSpan: (_name: string, fn: Function) => fn({ end: () => {}, setAttributes: () => {} }),
}),
};
trace.setGlobalTracerProvider(fakeProvider as any);

// Sentry.init() must call trace.disable() to clear the fake provider above
import * as Sentry from '@sentry/deno';
import { generateText } from 'ai';
import { MockLanguageModelV1 } from 'ai/test';
import { z } from 'zod';

Sentry.init({
traceLifecycle: 'static',
environment: 'qa',
dsn: Deno.env.get('E2E_TEST_DSN'),
debug: !!Deno.env.get('DEBUG'),
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1,
});

const port = 3030;

Deno.serve({ port }, async (req: Request) => {
const url = new URL(req.url);

if (url.pathname === '/test-success') {
return new Response(JSON.stringify({ version: 'v1' }), {
headers: { 'Content-Type': 'application/json' },
});
}

if (url.pathname === '/test-error') {
const exceptionId = Sentry.captureException(new Error('This is an error'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test Sentry.startSpan — uses Sentry's internal pipeline
if (url.pathname === '/test-sentry-span') {
Sentry.startSpan({ name: 'test-sentry-span' }, () => {
// noop
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startSpan — goes through the global TracerProvider
if (url.pathname === '/test-otel-span') {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('test-otel-span');
span.end();
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startActiveSpan — what AI SDK and most instrumentations use
if (url.pathname === '/test-otel-active-span') {
const tracer = trace.getTracer('test-tracer');
tracer.startActiveSpan('test-otel-active-span', span => {
span.setAttributes({ 'test.active': true });
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test interop: OTel span inside a Sentry span
if (url.pathname === '/test-interop') {
Sentry.startSpan({ name: 'sentry-parent' }, () => {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('otel-child');
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test breadcrumbs: add a breadcrumb then capture an error
if (url.pathname === '/test-breadcrumb') {
Sentry.addBreadcrumb({
message: 'test-breadcrumb',
category: 'custom',
level: 'info',
});
const exceptionId = Sentry.captureException(new Error('breadcrumb-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test context: set user, tag, extra then capture an error
if (url.pathname === '/test-context') {
Sentry.setUser({ id: '123', email: 'test@sentry.io' });
Sentry.setTag('deno-runtime', 'true');
Sentry.setExtra('detail', { key: 'value' });
const exceptionId = Sentry.captureException(new Error('context-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test scope isolation: tags inside withScope do not leak
if (url.pathname === '/test-scope-isolation') {
let insideId: string | undefined;
let outsideId: string | undefined;

Sentry.withScope(scope => {
scope.setTag('isolated', 'yes');
insideId = Sentry.captureException(new Error('inside-scope'));
});

outsideId = Sentry.captureException(new Error('outside-scope'));

return new Response(JSON.stringify({ insideId, outsideId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test outbound fetch instrumentation
if (url.pathname === '/test-outgoing-fetch') {
const response = await Sentry.startSpan({ name: 'test-outgoing-fetch' }, async () => {
const res = await fetch('http://localhost:3030/test-success');
return res.json();
});
return new Response(JSON.stringify(response), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI: Vercel AI SDK generateText with mock model
if (url.pathname === '/test-ai') {
const results = await Sentry.startSpan({ op: 'function', name: 'ai-test' }, async () => {
// First call - telemetry enabled by default
const result1 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'First span here!',
}),
}),
prompt: 'Where is the first span?',
});

// Second call - explicitly enabled telemetry
const result2 = await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Second span here!',
}),
}),
prompt: 'Where is the second span?',
});

// Third call - with tool calls
const result3 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (args: { location: string }) => {
return `Weather in ${args.location}: Sunny, 72°F`;
},
},
},
prompt: 'What is the weather in San Francisco?',
});

// Fourth call - explicitly disabled telemetry, should not be captured
const result4 = await generateText({
experimental_telemetry: { isEnabled: false },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Should not be captured!',
}),
}),
prompt: 'Where is the disabled span?',
});

return {
result1: result1.text,
result2: result2.text,
result3: result3.text,
result4: result4.text,
};
});

return new Response(JSON.stringify(results), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI error: tool call that throws
if (url.pathname === '/test-ai-error') {
try {
await Sentry.startSpan({ op: 'function', name: 'ai-error-test' }, async () => {
await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (_args: { location: string }) => {
throw new Error('Tool call failed');
},
},
},
prompt: 'What is the weather in San Francisco?',
});
});
} catch (e) {
Sentry.captureException(e);
}

return new Response(JSON.stringify({ status: 'error-handled' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test metrics: emit counter, distribution, and gauge
if (url.pathname === '/test-metrics') {
Sentry.metrics.count('test.deno.count', 1, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Apples',
},
});
Sentry.metrics.distribution('test.deno.distribution', 100, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Bananas',
},
});
Sentry.metrics.gauge('test.deno.gauge', 200, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Cherries',
},
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test logs: emit a debug log via Sentry.logger
if (url.pathname === '/test-log') {
Sentry.logger.debug('Accessed /test-log route');
return new Response(JSON.stringify({ message: 'Log sent' }), {
headers: { 'Content-Type': 'application/json' },
});
}

return new Response('Not found', { status: 404 });
});

console.log(`Deno test app listening on port ${port}`);
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'deno-streamed',
proxyServerName: 'deno-static',
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1041,7 +1041,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Deno
if:
matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed' || matrix.test-application ==
matrix.test-application == 'deno' || matrix.test-application == 'deno-static' || matrix.test-application ==
'deno-redis' || matrix.test-application == 'hono-4' || matrix.test-application == 'deno-mysql' ||
matrix.test-application == 'deno-pg'
uses: denoland/setup-deno@v2.0.5
Expand DownExpand Up@@ -1166,7 +1166,7 @@ jobs:
with:
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Set up Deno
if: matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed'
if: matrix.test-application == 'deno'
uses: denoland/setup-deno@v2.0.5
with:
deno-version: ${{ matrix.deno-version || 'v2.8.3' }}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
{
"name": "deno-streamed-app",
"name": "deno-static-app",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "deno run --allow-net --allow-env --allow-read --allow-sys src/app.ts",
"start": "deno run --allow-net --allow-env --allow-read src/app.ts",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api": "^1.9.0",
"ai": "^3.0.0",
"zod": "^3.22.4"
},
Expand All@@ -21,13 +21,5 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"optionalVariants": [
{
"deno-version": "latest",
"label": "deno-streamed (latest)"
}
]
}
}
307 changes: 307 additions & 0 deletions dev-packages/e2e-tests/test-applications/deno-static/src/app.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
import { trace } from '@opentelemetry/api';

// Simulate a pre-existing OTel provider (like Supabase Edge Runtime registers
// before user code runs). Without trace.disable() in Sentry's setup, this would
// cause setGlobalTracerProvider to be a no-op, silently dropping all OTel spans.
const fakeProvider = {
getTracer: () => ({
startSpan: () => ({ end: () => {}, setAttributes: () => {} }),
startActiveSpan: (_name: string, fn: Function) => fn({ end: () => {}, setAttributes: () => {} }),
}),
};
trace.setGlobalTracerProvider(fakeProvider as any);

// Sentry.init() must call trace.disable() to clear the fake provider above
import * as Sentry from '@sentry/deno';
import { generateText } from 'ai';
import { MockLanguageModelV1 } from 'ai/test';
import { z } from 'zod';

Sentry.init({
traceLifecycle: 'static',
environment: 'qa',
dsn: Deno.env.get('E2E_TEST_DSN'),
debug: !!Deno.env.get('DEBUG'),
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1,
});

const port = 3030;

Deno.serve({ port }, async (req: Request) => {
const url = new URL(req.url);

if (url.pathname === '/test-success') {
return new Response(JSON.stringify({ version: 'v1' }), {
headers: { 'Content-Type': 'application/json' },
});
}

if (url.pathname === '/test-error') {
const exceptionId = Sentry.captureException(new Error('This is an error'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test Sentry.startSpan — uses Sentry's internal pipeline
if (url.pathname === '/test-sentry-span') {
Sentry.startSpan({ name: 'test-sentry-span' }, () => {
// noop
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startSpan — goes through the global TracerProvider
if (url.pathname === '/test-otel-span') {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('test-otel-span');
span.end();
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startActiveSpan — what AI SDK and most instrumentations use
if (url.pathname === '/test-otel-active-span') {
const tracer = trace.getTracer('test-tracer');
tracer.startActiveSpan('test-otel-active-span', span => {
span.setAttributes({ 'test.active': true });
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test interop: OTel span inside a Sentry span
if (url.pathname === '/test-interop') {
Sentry.startSpan({ name: 'sentry-parent' }, () => {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('otel-child');
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test breadcrumbs: add a breadcrumb then capture an error
if (url.pathname === '/test-breadcrumb') {
Sentry.addBreadcrumb({
message: 'test-breadcrumb',
category: 'custom',
level: 'info',
});
const exceptionId = Sentry.captureException(new Error('breadcrumb-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test context: set user, tag, extra then capture an error
if (url.pathname === '/test-context') {
Sentry.setUser({ id: '123', email: 'test@sentry.io' });
Sentry.setTag('deno-runtime', 'true');
Sentry.setExtra('detail', { key: 'value' });
const exceptionId = Sentry.captureException(new Error('context-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test scope isolation: tags inside withScope do not leak
if (url.pathname === '/test-scope-isolation') {
let insideId: string | undefined;
let outsideId: string | undefined;

Sentry.withScope(scope => {
scope.setTag('isolated', 'yes');
insideId = Sentry.captureException(new Error('inside-scope'));
});

outsideId = Sentry.captureException(new Error('outside-scope'));

return new Response(JSON.stringify({ insideId, outsideId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test outbound fetch instrumentation
if (url.pathname === '/test-outgoing-fetch') {
const response = await Sentry.startSpan({ name: 'test-outgoing-fetch' }, async () => {
const res = await fetch('http://localhost:3030/test-success');
return res.json();
});
return new Response(JSON.stringify(response), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI: Vercel AI SDK generateText with mock model
if (url.pathname === '/test-ai') {
const results = await Sentry.startSpan({ op: 'function', name: 'ai-test' }, async () => {
// First call - telemetry enabled by default
const result1 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'First span here!',
}),
}),
prompt: 'Where is the first span?',
});

// Second call - explicitly enabled telemetry
const result2 = await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Second span here!',
}),
}),
prompt: 'Where is the second span?',
});

// Third call - with tool calls
const result3 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (args: { location: string }) => {
return `Weather in ${args.location}: Sunny, 72°F`;
},
},
},
prompt: 'What is the weather in San Francisco?',
});

// Fourth call - explicitly disabled telemetry, should not be captured
const result4 = await generateText({
experimental_telemetry: { isEnabled: false },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Should not be captured!',
}),
}),
prompt: 'Where is the disabled span?',
});

return {
result1: result1.text,
result2: result2.text,
result3: result3.text,
result4: result4.text,
};
});

return new Response(JSON.stringify(results), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI error: tool call that throws
if (url.pathname === '/test-ai-error') {
try {
await Sentry.startSpan({ op: 'function', name: 'ai-error-test' }, async () => {
await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (_args: { location: string }) => {
throw new Error('Tool call failed');
},
},
},
prompt: 'What is the weather in San Francisco?',
});
});
} catch (e) {
Sentry.captureException(e);
}

return new Response(JSON.stringify({ status: 'error-handled' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test metrics: emit counter, distribution, and gauge
if (url.pathname === '/test-metrics') {
Sentry.metrics.count('test.deno.count', 1, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Apples',
},
});
Sentry.metrics.distribution('test.deno.distribution', 100, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Bananas',
},
});
Sentry.metrics.gauge('test.deno.gauge', 200, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Cherries',
},
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test logs: emit a debug log via Sentry.logger
if (url.pathname === '/test-log') {
Sentry.logger.debug('Accessed /test-log route');
return new Response(JSON.stringify({ message: 'Log sent' }), {
headers: { 'Content-Type': 'application/json' },
});
}

return new Response('Not found', { status: 404 });
});

console.log(`Deno test app listening on port ${port}`);
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'deno-streamed',
proxyServerName: 'deno-static',
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1041,7 +1041,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Deno
if:
matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed' || matrix.test-application ==
matrix.test-application == 'deno' || matrix.test-application == 'deno-static' || matrix.test-application ==
'deno-redis' || matrix.test-application == 'hono-4' || matrix.test-application == 'deno-mysql' ||
matrix.test-application == 'deno-pg'
uses: denoland/setup-deno@v2.0.5
Expand DownExpand Up@@ -1166,7 +1166,7 @@ jobs:
with:
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Set up Deno
if: matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed'
if: matrix.test-application == 'deno'
uses: denoland/setup-deno@v2.0.5
with:
deno-version: ${{ matrix.deno-version || 'v2.8.3' }}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
{
"name": "deno-streamed-app",
"name": "deno-static-app",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "deno run --allow-net --allow-env --allow-read --allow-sys src/app.ts",
"start": "deno run --allow-net --allow-env --allow-read src/app.ts",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api": "^1.9.0",
"ai": "^3.0.0",
"zod": "^3.22.4"
},
Expand All@@ -21,13 +21,5 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"optionalVariants": [
{
"deno-version": "latest",
"label": "deno-streamed (latest)"
}
]
}
}
307 changes: 307 additions & 0 deletions dev-packages/e2e-tests/test-applications/deno-static/src/app.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
import { trace } from '@opentelemetry/api';

// Simulate a pre-existing OTel provider (like Supabase Edge Runtime registers
// before user code runs). Without trace.disable() in Sentry's setup, this would
// cause setGlobalTracerProvider to be a no-op, silently dropping all OTel spans.
const fakeProvider = {
getTracer: () => ({
startSpan: () => ({ end: () => {}, setAttributes: () => {} }),
startActiveSpan: (_name: string, fn: Function) => fn({ end: () => {}, setAttributes: () => {} }),
}),
};
trace.setGlobalTracerProvider(fakeProvider as any);

// Sentry.init() must call trace.disable() to clear the fake provider above
import * as Sentry from '@sentry/deno';
import { generateText } from 'ai';
import { MockLanguageModelV1 } from 'ai/test';
import { z } from 'zod';

Sentry.init({
traceLifecycle: 'static',
environment: 'qa',
dsn: Deno.env.get('E2E_TEST_DSN'),
debug: !!Deno.env.get('DEBUG'),
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1,
});

const port = 3030;

Deno.serve({ port }, async (req: Request) => {
const url = new URL(req.url);

if (url.pathname === '/test-success') {
return new Response(JSON.stringify({ version: 'v1' }), {
headers: { 'Content-Type': 'application/json' },
});
}

if (url.pathname === '/test-error') {
const exceptionId = Sentry.captureException(new Error('This is an error'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test Sentry.startSpan — uses Sentry's internal pipeline
if (url.pathname === '/test-sentry-span') {
Sentry.startSpan({ name: 'test-sentry-span' }, () => {
// noop
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startSpan — goes through the global TracerProvider
if (url.pathname === '/test-otel-span') {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('test-otel-span');
span.end();
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startActiveSpan — what AI SDK and most instrumentations use
if (url.pathname === '/test-otel-active-span') {
const tracer = trace.getTracer('test-tracer');
tracer.startActiveSpan('test-otel-active-span', span => {
span.setAttributes({ 'test.active': true });
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test interop: OTel span inside a Sentry span
if (url.pathname === '/test-interop') {
Sentry.startSpan({ name: 'sentry-parent' }, () => {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('otel-child');
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test breadcrumbs: add a breadcrumb then capture an error
if (url.pathname === '/test-breadcrumb') {
Sentry.addBreadcrumb({
message: 'test-breadcrumb',
category: 'custom',
level: 'info',
});
const exceptionId = Sentry.captureException(new Error('breadcrumb-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test context: set user, tag, extra then capture an error
if (url.pathname === '/test-context') {
Sentry.setUser({ id: '123', email: 'test@sentry.io' });
Sentry.setTag('deno-runtime', 'true');
Sentry.setExtra('detail', { key: 'value' });
const exceptionId = Sentry.captureException(new Error('context-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test scope isolation: tags inside withScope do not leak
if (url.pathname === '/test-scope-isolation') {
let insideId: string | undefined;
let outsideId: string | undefined;

Sentry.withScope(scope => {
scope.setTag('isolated', 'yes');
insideId = Sentry.captureException(new Error('inside-scope'));
});

outsideId = Sentry.captureException(new Error('outside-scope'));

return new Response(JSON.stringify({ insideId, outsideId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test outbound fetch instrumentation
if (url.pathname === '/test-outgoing-fetch') {
const response = await Sentry.startSpan({ name: 'test-outgoing-fetch' }, async () => {
const res = await fetch('http://localhost:3030/test-success');
return res.json();
});
return new Response(JSON.stringify(response), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI: Vercel AI SDK generateText with mock model
if (url.pathname === '/test-ai') {
const results = await Sentry.startSpan({ op: 'function', name: 'ai-test' }, async () => {
// First call - telemetry enabled by default
const result1 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'First span here!',
}),
}),
prompt: 'Where is the first span?',
});

// Second call - explicitly enabled telemetry
const result2 = await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Second span here!',
}),
}),
prompt: 'Where is the second span?',
});

// Third call - with tool calls
const result3 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (args: { location: string }) => {
return `Weather in ${args.location}: Sunny, 72°F`;
},
},
},
prompt: 'What is the weather in San Francisco?',
});

// Fourth call - explicitly disabled telemetry, should not be captured
const result4 = await generateText({
experimental_telemetry: { isEnabled: false },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Should not be captured!',
}),
}),
prompt: 'Where is the disabled span?',
});

return {
result1: result1.text,
result2: result2.text,
result3: result3.text,
result4: result4.text,
};
});

return new Response(JSON.stringify(results), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI error: tool call that throws
if (url.pathname === '/test-ai-error') {
try {
await Sentry.startSpan({ op: 'function', name: 'ai-error-test' }, async () => {
await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (_args: { location: string }) => {
throw new Error('Tool call failed');
},
},
},
prompt: 'What is the weather in San Francisco?',
});
});
} catch (e) {
Sentry.captureException(e);
}

return new Response(JSON.stringify({ status: 'error-handled' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test metrics: emit counter, distribution, and gauge
if (url.pathname === '/test-metrics') {
Sentry.metrics.count('test.deno.count', 1, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Apples',
},
});
Sentry.metrics.distribution('test.deno.distribution', 100, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Bananas',
},
});
Sentry.metrics.gauge('test.deno.gauge', 200, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Cherries',
},
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test logs: emit a debug log via Sentry.logger
if (url.pathname === '/test-log') {
Sentry.logger.debug('Accessed /test-log route');
return new Response(JSON.stringify({ message: 'Log sent' }), {
headers: { 'Content-Type': 'application/json' },
});
}

return new Response('Not found', { status: 404 });
});

console.log(`Deno test app listening on port ${port}`);
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'deno-streamed',
proxyServerName: 'deno-static',
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -1041,7 +1041,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Deno
if:
matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed' || matrix.test-application ==
matrix.test-application == 'deno' || matrix.test-application == 'deno-static' || matrix.test-application ==
'deno-redis' || matrix.test-application == 'hono-4' || matrix.test-application == 'deno-mysql' ||
matrix.test-application == 'deno-pg'
uses: denoland/setup-deno@v2.0.5
Expand DownExpand Up@@ -1166,7 +1166,7 @@ jobs:
with:
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Set up Deno
if: matrix.test-application == 'deno' || matrix.test-application == 'deno-streamed'
if: matrix.test-application == 'deno'
uses: denoland/setup-deno@v2.0.5
with:
deno-version: ${{ matrix.deno-version || 'v2.8.3' }}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
{
"name": "deno-streamed-app",
"name": "deno-static-app",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "deno run --allow-net --allow-env --allow-read --allow-sys src/app.ts",
"start": "deno run --allow-net --allow-env --allow-read src/app.ts",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api": "^1.9.0",
"ai": "^3.0.0",
"zod": "^3.22.4"
},
Expand All@@ -21,13 +21,5 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"optionalVariants": [
{
"deno-version": "latest",
"label": "deno-streamed (latest)"
}
]
}
}
307 changes: 307 additions & 0 deletions dev-packages/e2e-tests/test-applications/deno-static/src/app.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
import { trace } from '@opentelemetry/api';

// Simulate a pre-existing OTel provider (like Supabase Edge Runtime registers
// before user code runs). Without trace.disable() in Sentry's setup, this would
// cause setGlobalTracerProvider to be a no-op, silently dropping all OTel spans.
const fakeProvider = {
getTracer: () => ({
startSpan: () => ({ end: () => {}, setAttributes: () => {} }),
startActiveSpan: (_name: string, fn: Function) => fn({ end: () => {}, setAttributes: () => {} }),
}),
};
trace.setGlobalTracerProvider(fakeProvider as any);

// Sentry.init() must call trace.disable() to clear the fake provider above
import * as Sentry from '@sentry/deno';
import { generateText } from 'ai';
import { MockLanguageModelV1 } from 'ai/test';
import { z } from 'zod';

Sentry.init({
traceLifecycle: 'static',
environment: 'qa',
dsn: Deno.env.get('E2E_TEST_DSN'),
debug: !!Deno.env.get('DEBUG'),
tunnel: 'http://localhost:3031/',
tracesSampleRate: 1,
});

const port = 3030;

Deno.serve({ port }, async (req: Request) => {
const url = new URL(req.url);

if (url.pathname === '/test-success') {
return new Response(JSON.stringify({ version: 'v1' }), {
headers: { 'Content-Type': 'application/json' },
});
}

if (url.pathname === '/test-error') {
const exceptionId = Sentry.captureException(new Error('This is an error'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test Sentry.startSpan — uses Sentry's internal pipeline
if (url.pathname === '/test-sentry-span') {
Sentry.startSpan({ name: 'test-sentry-span' }, () => {
// noop
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startSpan — goes through the global TracerProvider
if (url.pathname === '/test-otel-span') {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('test-otel-span');
span.end();
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test OTel tracer.startActiveSpan — what AI SDK and most instrumentations use
if (url.pathname === '/test-otel-active-span') {
const tracer = trace.getTracer('test-tracer');
tracer.startActiveSpan('test-otel-active-span', span => {
span.setAttributes({ 'test.active': true });
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test interop: OTel span inside a Sentry span
if (url.pathname === '/test-interop') {
Sentry.startSpan({ name: 'sentry-parent' }, () => {
const tracer = trace.getTracer('test-tracer');
const span = tracer.startSpan('otel-child');
span.end();
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test breadcrumbs: add a breadcrumb then capture an error
if (url.pathname === '/test-breadcrumb') {
Sentry.addBreadcrumb({
message: 'test-breadcrumb',
category: 'custom',
level: 'info',
});
const exceptionId = Sentry.captureException(new Error('breadcrumb-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test context: set user, tag, extra then capture an error
if (url.pathname === '/test-context') {
Sentry.setUser({ id: '123', email: 'test@sentry.io' });
Sentry.setTag('deno-runtime', 'true');
Sentry.setExtra('detail', { key: 'value' });
const exceptionId = Sentry.captureException(new Error('context-test'));
return new Response(JSON.stringify({ exceptionId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test scope isolation: tags inside withScope do not leak
if (url.pathname === '/test-scope-isolation') {
let insideId: string | undefined;
let outsideId: string | undefined;

Sentry.withScope(scope => {
scope.setTag('isolated', 'yes');
insideId = Sentry.captureException(new Error('inside-scope'));
});

outsideId = Sentry.captureException(new Error('outside-scope'));

return new Response(JSON.stringify({ insideId, outsideId }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test outbound fetch instrumentation
if (url.pathname === '/test-outgoing-fetch') {
const response = await Sentry.startSpan({ name: 'test-outgoing-fetch' }, async () => {
const res = await fetch('http://localhost:3030/test-success');
return res.json();
});
return new Response(JSON.stringify(response), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI: Vercel AI SDK generateText with mock model
if (url.pathname === '/test-ai') {
const results = await Sentry.startSpan({ op: 'function', name: 'ai-test' }, async () => {
// First call - telemetry enabled by default
const result1 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'First span here!',
}),
}),
prompt: 'Where is the first span?',
});

// Second call - explicitly enabled telemetry
const result2 = await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Second span here!',
}),
}),
prompt: 'Where is the second span?',
});

// Third call - with tool calls
const result3 = await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (args: { location: string }) => {
return `Weather in ${args.location}: Sunny, 72°F`;
},
},
},
prompt: 'What is the weather in San Francisco?',
});

// Fourth call - explicitly disabled telemetry, should not be captured
const result4 = await generateText({
experimental_telemetry: { isEnabled: false },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: 'Should not be captured!',
}),
}),
prompt: 'Where is the disabled span?',
});

return {
result1: result1.text,
result2: result2.text,
result3: result3.text,
result4: result4.text,
};
});

return new Response(JSON.stringify(results), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test AI error: tool call that throws
if (url.pathname === '/test-ai-error') {
try {
await Sentry.startSpan({ op: 'function', name: 'ai-error-test' }, async () => {
await generateText({
experimental_telemetry: { isEnabled: true },
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async (_args: { location: string }) => {
throw new Error('Tool call failed');
},
},
},
prompt: 'What is the weather in San Francisco?',
});
});
} catch (e) {
Sentry.captureException(e);
}

return new Response(JSON.stringify({ status: 'error-handled' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test metrics: emit counter, distribution, and gauge
if (url.pathname === '/test-metrics') {
Sentry.metrics.count('test.deno.count', 1, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Apples',
},
});
Sentry.metrics.distribution('test.deno.distribution', 100, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Bananas',
},
});
Sentry.metrics.gauge('test.deno.gauge', 200, {
attributes: {
endpoint: '/test-metrics',
'random.attribute': 'Cherries',
},
});
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' },
});
}

// Test logs: emit a debug log via Sentry.logger
if (url.pathname === '/test-log') {
Sentry.logger.debug('Accessed /test-log route');
return new Response(JSON.stringify({ message: 'Log sent' }), {
headers: { 'Content-Type': 'application/json' },
});
}

return new Response('Not found', { status: 404 });
});

console.log(`Deno test app listening on port ${port}`);
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'deno-streamed',
proxyServerName: 'deno-static',
});
Loading
Loading