Merged
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
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -485,7 +485,7 @@ module.exports = [
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: false,
brotli: false,
limit: '522 KiB',
limit: '540 KiB',
disablePlugins: ['@size-limit/webpack'],
webpack: false,
modifyEsbuildConfig: function (config) {
Expand Down
15 changes: 14 additions & 1 deletion dev-packages/cloudflare-integration-tests/runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -161,6 +161,7 @@ export function createRunner(...paths: string[]) {

// controls whether envelopes are expected in predefined order or not
let unordered = false;
let failOnUnexpected = false;

if (!existsSync(testPath)) {
throw new Error(`Test scenario not found: ${testPath}`);
Expand DownExpand Up@@ -195,6 +196,10 @@ export function createRunner(...paths: string[]) {
unordered = true;
return this;
},
failOnUnexpected: function () {
failOnUnexpected = true;
return this;
},
ignore: function (...types: EnvelopeItemType[]) {
types.forEach(t => ignored.add(t));
return this;
Expand DownExpand Up@@ -222,6 +227,7 @@ export function createRunner(...paths: string[]) {
const expectedEnvelopeCount = expectedEnvelopes.length;

let envelopeCount = 0;
let unexpectedEnvelopeError: Error | undefined;
const envelopeWaiters: { expected: Expected; resolve: () => void; reject: (e: unknown) => void }[] = [];
const {
resolve: setWorkerPort,
Expand DownExpand Up@@ -297,6 +303,10 @@ export function createRunner(...paths: string[]) {

// no match found
if (matchIndex < 0) {
if (failOnUnexpected) {
unexpectedEnvelopeError ??= new Error('Received an unexpected envelope');
reject(unexpectedEnvelopeError);
}
return;
}

Expand DownExpand Up@@ -429,7 +439,10 @@ export function createRunner(...paths: string[]) {

return {
completed: async function (): Promise<void> {
return isComplete;
await isComplete;
if (unexpectedEnvelopeError) {
throw unexpectedEnvelopeError;
}
},
makeRequest: async function <T>(
method: 'get' | 'post',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
CACHE_DO: DurableObjectNamespace;
NO_CACHE_DO: DurableObjectNamespace;
}

/**
* Sync KV and SQL work against the DO's own storage, which the SDK instruments into `db` spans.
* Used to check that those spans still reach the transport from inside a Durable Object, where a
* cached client never hits an invocation-boundary flush and has to rely on the eager drain.
*/
function runStorageOps(ctx: DurableObjectState): { listSize: number; rows: number } {
ctx.storage.kv.put('cache-key', { hello: 'sync' });
ctx.storage.kv.get('cache-key');
const entries = [...ctx.storage.kv.list()];
ctx.storage.kv.delete('cache-key');

ctx.storage.sql.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
ctx.storage.sql.exec('INSERT INTO users (name) VALUES (?)', 'Alice');
const rows = ctx.storage.sql.exec('SELECT * FROM users').toArray();

return { listSize: entries.length, rows: rows.length };
}

function startDetachedWork(message: string): string {
void (async () => {
await new Promise(r => setTimeout(r, 3000));
await Sentry.startSpan({ name: 'do.detached-task', op: 'task' }, async () => {
Sentry.logger.info(`Detached log: ${message}`);
Sentry.metrics.count('do.detached', 1);
Sentry.captureException(new Error(message));
});
})();
return `Detached work started: ${message}`;
}

// DO with cacheClient: true (the default) — detached work events SHOULD be captured
class CacheDurableObjectBase extends DurableObject<Env> {
async echo(n: number): Promise<number> {
return n;
}

async handlerError(instanceId: string): Promise<void> {
throw new Error(`Cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async scopeCheck(seed: boolean): Promise<string> {
if (seed) {
Sentry.setTag('seeded_tag', 'from-seeding-call');
Sentry.setUser({ id: 'user-from-seeding-call' });
}
Sentry.captureException(new Error(seed ? 'Cache scope seed' : 'Cache scope probe'));
return 'ok';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from cache DO ${url.searchParams.get('id')}`));
}
if (url.pathname === '/streaming') {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('chunk1'));
controller.enqueue(new TextEncoder().encode('chunk2'));
controller.close();
},
});
return new Response(stream, { headers: { 'content-type': 'text/event-stream' } });
}
return new Response('Cache DO');
}
}

// DO with cacheClient: false — detached work events should NOT be captured
class NoCacheDurableObjectBase extends DurableObject<Env> {
async handlerError(instanceId: string): Promise<void> {
throw new Error(`No-cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `no-cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from no-cache DO ${url.searchParams.get('id')}`));
}
return new Response('No-cache DO');
}
}

export const CacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
CacheDurableObjectBase,
);

export const NoCacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
cacheClient: false,
enableRpcTracePropagation: true,
}),
NoCacheDurableObjectBase,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
{
async fetch(request, env, ctx) {
const url = new URL(request.url);
const instanceId = url.searchParams.get('id') || 'default';
const errorMessage = url.searchParams.get('errorMessage') || 'Same error';

// Work that finishes AFTER the response: a post-response span tree plus a
// log, metric and error, all registered via waitUntil. This is the worker-side
// half of the #22545 lifecycle (the DO-side half is /detached).
if (url.pathname === '/post-response') {
ctx.waitUntil(
Sentry.startSpan({ name: 'checkout.post-response', op: 'task' }, async () => {
Sentry.logger.info('checkout post-response log');
Sentry.metrics.count('checkout.processed', 1);
await new Promise(r => setTimeout(r, 50));
await Sentry.startSpan({ name: 'checkout.notify-webhook', op: 'http.client' }, async () => {
await new Promise(r => setTimeout(r, 25));
Sentry.captureException(new Error('Webhook delivery failed'));
});
}),
);
return new Response('checkout accepted');
}

// Fan a single request out into N sequential DO RPC calls — every RPC span must
// land in this request's trace when RPC trace propagation is on.
if (url.pathname === '/burst') {
const n = Math.min(Number(url.searchParams.get('n')) || 1, 20);
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`burst-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;

let sum = 0;
for (let i = 0; i < n; i++) {
sum += (await stub.echo(i)) as number;
}
return Response.json({ calls: n, sum });
}

// Cache DO RPC calls
if (url.pathname === '/cache/handler-error') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/cache/dedupe') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/cache/scope') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.scopeCheck(url.searchParams.get('seed') === '1'));
}

// Cache DO fetch calls — detached work goes through fetch (matching the #22545 repro),
// since the DO fetch handler always initializes the DO's own client
if (url.pathname === '/cache/detached') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

if (url.pathname === '/cache/storage') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/cache/streaming') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request('http://do/streaming'));
}

// No-cache DO calls
if (url.pathname === '/no-cache/handler-error') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/no-cache/dedupe') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/no-cache/storage') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/no-cache/detached') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

return new Response('Hello World!');
},
} satisfies ExportedHandler<Env>,
);
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
Merged
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
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -485,7 +485,7 @@ module.exports = [
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: false,
brotli: false,
limit: '522 KiB',
limit: '540 KiB',
disablePlugins: ['@size-limit/webpack'],
webpack: false,
modifyEsbuildConfig: function (config) {
Expand Down
15 changes: 14 additions & 1 deletion dev-packages/cloudflare-integration-tests/runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -161,6 +161,7 @@ export function createRunner(...paths: string[]) {

// controls whether envelopes are expected in predefined order or not
let unordered = false;
let failOnUnexpected = false;

if (!existsSync(testPath)) {
throw new Error(`Test scenario not found: ${testPath}`);
Expand DownExpand Up@@ -195,6 +196,10 @@ export function createRunner(...paths: string[]) {
unordered = true;
return this;
},
failOnUnexpected: function () {
failOnUnexpected = true;
return this;
},
ignore: function (...types: EnvelopeItemType[]) {
types.forEach(t => ignored.add(t));
return this;
Expand DownExpand Up@@ -222,6 +227,7 @@ export function createRunner(...paths: string[]) {
const expectedEnvelopeCount = expectedEnvelopes.length;

let envelopeCount = 0;
let unexpectedEnvelopeError: Error | undefined;
const envelopeWaiters: { expected: Expected; resolve: () => void; reject: (e: unknown) => void }[] = [];
const {
resolve: setWorkerPort,
Expand DownExpand Up@@ -297,6 +303,10 @@ export function createRunner(...paths: string[]) {

// no match found
if (matchIndex < 0) {
if (failOnUnexpected) {
unexpectedEnvelopeError ??= new Error('Received an unexpected envelope');
reject(unexpectedEnvelopeError);
}
return;
}

Expand DownExpand Up@@ -429,7 +439,10 @@ export function createRunner(...paths: string[]) {

return {
completed: async function (): Promise<void> {
return isComplete;
await isComplete;
if (unexpectedEnvelopeError) {
throw unexpectedEnvelopeError;
}
},
makeRequest: async function <T>(
method: 'get' | 'post',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
CACHE_DO: DurableObjectNamespace;
NO_CACHE_DO: DurableObjectNamespace;
}

/**
* Sync KV and SQL work against the DO's own storage, which the SDK instruments into `db` spans.
* Used to check that those spans still reach the transport from inside a Durable Object, where a
* cached client never hits an invocation-boundary flush and has to rely on the eager drain.
*/
function runStorageOps(ctx: DurableObjectState): { listSize: number; rows: number } {
ctx.storage.kv.put('cache-key', { hello: 'sync' });
ctx.storage.kv.get('cache-key');
const entries = [...ctx.storage.kv.list()];
ctx.storage.kv.delete('cache-key');

ctx.storage.sql.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
ctx.storage.sql.exec('INSERT INTO users (name) VALUES (?)', 'Alice');
const rows = ctx.storage.sql.exec('SELECT * FROM users').toArray();

return { listSize: entries.length, rows: rows.length };
}

function startDetachedWork(message: string): string {
void (async () => {
await new Promise(r => setTimeout(r, 3000));
await Sentry.startSpan({ name: 'do.detached-task', op: 'task' }, async () => {
Sentry.logger.info(`Detached log: ${message}`);
Sentry.metrics.count('do.detached', 1);
Sentry.captureException(new Error(message));
});
})();
return `Detached work started: ${message}`;
}

// DO with cacheClient: true (the default) — detached work events SHOULD be captured
class CacheDurableObjectBase extends DurableObject<Env> {
async echo(n: number): Promise<number> {
return n;
}

async handlerError(instanceId: string): Promise<void> {
throw new Error(`Cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async scopeCheck(seed: boolean): Promise<string> {
if (seed) {
Sentry.setTag('seeded_tag', 'from-seeding-call');
Sentry.setUser({ id: 'user-from-seeding-call' });
}
Sentry.captureException(new Error(seed ? 'Cache scope seed' : 'Cache scope probe'));
return 'ok';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from cache DO ${url.searchParams.get('id')}`));
}
if (url.pathname === '/streaming') {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('chunk1'));
controller.enqueue(new TextEncoder().encode('chunk2'));
controller.close();
},
});
return new Response(stream, { headers: { 'content-type': 'text/event-stream' } });
}
return new Response('Cache DO');
}
}

// DO with cacheClient: false — detached work events should NOT be captured
class NoCacheDurableObjectBase extends DurableObject<Env> {
async handlerError(instanceId: string): Promise<void> {
throw new Error(`No-cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `no-cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from no-cache DO ${url.searchParams.get('id')}`));
}
return new Response('No-cache DO');
}
}

export const CacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
CacheDurableObjectBase,
);

export const NoCacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
cacheClient: false,
enableRpcTracePropagation: true,
}),
NoCacheDurableObjectBase,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
{
async fetch(request, env, ctx) {
const url = new URL(request.url);
const instanceId = url.searchParams.get('id') || 'default';
const errorMessage = url.searchParams.get('errorMessage') || 'Same error';

// Work that finishes AFTER the response: a post-response span tree plus a
// log, metric and error, all registered via waitUntil. This is the worker-side
// half of the #22545 lifecycle (the DO-side half is /detached).
if (url.pathname === '/post-response') {
ctx.waitUntil(
Sentry.startSpan({ name: 'checkout.post-response', op: 'task' }, async () => {
Sentry.logger.info('checkout post-response log');
Sentry.metrics.count('checkout.processed', 1);
await new Promise(r => setTimeout(r, 50));
await Sentry.startSpan({ name: 'checkout.notify-webhook', op: 'http.client' }, async () => {
await new Promise(r => setTimeout(r, 25));
Sentry.captureException(new Error('Webhook delivery failed'));
});
}),
);
return new Response('checkout accepted');
}

// Fan a single request out into N sequential DO RPC calls — every RPC span must
// land in this request's trace when RPC trace propagation is on.
if (url.pathname === '/burst') {
const n = Math.min(Number(url.searchParams.get('n')) || 1, 20);
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`burst-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;

let sum = 0;
for (let i = 0; i < n; i++) {
sum += (await stub.echo(i)) as number;
}
return Response.json({ calls: n, sum });
}

// Cache DO RPC calls
if (url.pathname === '/cache/handler-error') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/cache/dedupe') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/cache/scope') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.scopeCheck(url.searchParams.get('seed') === '1'));
}

// Cache DO fetch calls — detached work goes through fetch (matching the #22545 repro),
// since the DO fetch handler always initializes the DO's own client
if (url.pathname === '/cache/detached') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

if (url.pathname === '/cache/storage') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/cache/streaming') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request('http://do/streaming'));
}

// No-cache DO calls
if (url.pathname === '/no-cache/handler-error') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/no-cache/dedupe') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/no-cache/storage') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/no-cache/detached') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

return new Response('Hello World!');
},
} satisfies ExportedHandler<Env>,
);
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
Merged
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
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -485,7 +485,7 @@ module.exports = [
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: false,
brotli: false,
limit: '522 KiB',
limit: '540 KiB',
disablePlugins: ['@size-limit/webpack'],
webpack: false,
modifyEsbuildConfig: function (config) {
Expand Down
15 changes: 14 additions & 1 deletion dev-packages/cloudflare-integration-tests/runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -161,6 +161,7 @@ export function createRunner(...paths: string[]) {

// controls whether envelopes are expected in predefined order or not
let unordered = false;
let failOnUnexpected = false;

if (!existsSync(testPath)) {
throw new Error(`Test scenario not found: ${testPath}`);
Expand DownExpand Up@@ -195,6 +196,10 @@ export function createRunner(...paths: string[]) {
unordered = true;
return this;
},
failOnUnexpected: function () {
failOnUnexpected = true;
return this;
},
ignore: function (...types: EnvelopeItemType[]) {
types.forEach(t => ignored.add(t));
return this;
Expand DownExpand Up@@ -222,6 +227,7 @@ export function createRunner(...paths: string[]) {
const expectedEnvelopeCount = expectedEnvelopes.length;

let envelopeCount = 0;
let unexpectedEnvelopeError: Error | undefined;
const envelopeWaiters: { expected: Expected; resolve: () => void; reject: (e: unknown) => void }[] = [];
const {
resolve: setWorkerPort,
Expand DownExpand Up@@ -297,6 +303,10 @@ export function createRunner(...paths: string[]) {

// no match found
if (matchIndex < 0) {
if (failOnUnexpected) {
unexpectedEnvelopeError ??= new Error('Received an unexpected envelope');
reject(unexpectedEnvelopeError);
}
return;
}

Expand DownExpand Up@@ -429,7 +439,10 @@ export function createRunner(...paths: string[]) {

return {
completed: async function (): Promise<void> {
return isComplete;
await isComplete;
if (unexpectedEnvelopeError) {
throw unexpectedEnvelopeError;
}
},
makeRequest: async function <T>(
method: 'get' | 'post',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
CACHE_DO: DurableObjectNamespace;
NO_CACHE_DO: DurableObjectNamespace;
}

/**
* Sync KV and SQL work against the DO's own storage, which the SDK instruments into `db` spans.
* Used to check that those spans still reach the transport from inside a Durable Object, where a
* cached client never hits an invocation-boundary flush and has to rely on the eager drain.
*/
function runStorageOps(ctx: DurableObjectState): { listSize: number; rows: number } {
ctx.storage.kv.put('cache-key', { hello: 'sync' });
ctx.storage.kv.get('cache-key');
const entries = [...ctx.storage.kv.list()];
ctx.storage.kv.delete('cache-key');

ctx.storage.sql.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
ctx.storage.sql.exec('INSERT INTO users (name) VALUES (?)', 'Alice');
const rows = ctx.storage.sql.exec('SELECT * FROM users').toArray();

return { listSize: entries.length, rows: rows.length };
}

function startDetachedWork(message: string): string {
void (async () => {
await new Promise(r => setTimeout(r, 3000));
await Sentry.startSpan({ name: 'do.detached-task', op: 'task' }, async () => {
Sentry.logger.info(`Detached log: ${message}`);
Sentry.metrics.count('do.detached', 1);
Sentry.captureException(new Error(message));
});
})();
return `Detached work started: ${message}`;
}

// DO with cacheClient: true (the default) — detached work events SHOULD be captured
class CacheDurableObjectBase extends DurableObject<Env> {
async echo(n: number): Promise<number> {
return n;
}

async handlerError(instanceId: string): Promise<void> {
throw new Error(`Cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async scopeCheck(seed: boolean): Promise<string> {
if (seed) {
Sentry.setTag('seeded_tag', 'from-seeding-call');
Sentry.setUser({ id: 'user-from-seeding-call' });
}
Sentry.captureException(new Error(seed ? 'Cache scope seed' : 'Cache scope probe'));
return 'ok';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from cache DO ${url.searchParams.get('id')}`));
}
if (url.pathname === '/streaming') {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('chunk1'));
controller.enqueue(new TextEncoder().encode('chunk2'));
controller.close();
},
});
return new Response(stream, { headers: { 'content-type': 'text/event-stream' } });
}
return new Response('Cache DO');
}
}

// DO with cacheClient: false — detached work events should NOT be captured
class NoCacheDurableObjectBase extends DurableObject<Env> {
async handlerError(instanceId: string): Promise<void> {
throw new Error(`No-cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `no-cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from no-cache DO ${url.searchParams.get('id')}`));
}
return new Response('No-cache DO');
}
}

export const CacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
CacheDurableObjectBase,
);

export const NoCacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
cacheClient: false,
enableRpcTracePropagation: true,
}),
NoCacheDurableObjectBase,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
{
async fetch(request, env, ctx) {
const url = new URL(request.url);
const instanceId = url.searchParams.get('id') || 'default';
const errorMessage = url.searchParams.get('errorMessage') || 'Same error';

// Work that finishes AFTER the response: a post-response span tree plus a
// log, metric and error, all registered via waitUntil. This is the worker-side
// half of the #22545 lifecycle (the DO-side half is /detached).
if (url.pathname === '/post-response') {
ctx.waitUntil(
Sentry.startSpan({ name: 'checkout.post-response', op: 'task' }, async () => {
Sentry.logger.info('checkout post-response log');
Sentry.metrics.count('checkout.processed', 1);
await new Promise(r => setTimeout(r, 50));
await Sentry.startSpan({ name: 'checkout.notify-webhook', op: 'http.client' }, async () => {
await new Promise(r => setTimeout(r, 25));
Sentry.captureException(new Error('Webhook delivery failed'));
});
}),
);
return new Response('checkout accepted');
}

// Fan a single request out into N sequential DO RPC calls — every RPC span must
// land in this request's trace when RPC trace propagation is on.
if (url.pathname === '/burst') {
const n = Math.min(Number(url.searchParams.get('n')) || 1, 20);
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`burst-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;

let sum = 0;
for (let i = 0; i < n; i++) {
sum += (await stub.echo(i)) as number;
}
return Response.json({ calls: n, sum });
}

// Cache DO RPC calls
if (url.pathname === '/cache/handler-error') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/cache/dedupe') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/cache/scope') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.scopeCheck(url.searchParams.get('seed') === '1'));
}

// Cache DO fetch calls — detached work goes through fetch (matching the #22545 repro),
// since the DO fetch handler always initializes the DO's own client
if (url.pathname === '/cache/detached') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

if (url.pathname === '/cache/storage') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/cache/streaming') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request('http://do/streaming'));
}

// No-cache DO calls
if (url.pathname === '/no-cache/handler-error') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/no-cache/dedupe') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/no-cache/storage') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/no-cache/detached') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

return new Response('Hello World!');
},
} satisfies ExportedHandler<Env>,
);
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
Merged
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
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -485,7 +485,7 @@ module.exports = [
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: false,
brotli: false,
limit: '522 KiB',
limit: '540 KiB',
disablePlugins: ['@size-limit/webpack'],
webpack: false,
modifyEsbuildConfig: function (config) {
Expand Down
15 changes: 14 additions & 1 deletion dev-packages/cloudflare-integration-tests/runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -161,6 +161,7 @@ export function createRunner(...paths: string[]) {

// controls whether envelopes are expected in predefined order or not
let unordered = false;
let failOnUnexpected = false;

if (!existsSync(testPath)) {
throw new Error(`Test scenario not found: ${testPath}`);
Expand DownExpand Up@@ -195,6 +196,10 @@ export function createRunner(...paths: string[]) {
unordered = true;
return this;
},
failOnUnexpected: function () {
failOnUnexpected = true;
return this;
},
ignore: function (...types: EnvelopeItemType[]) {
types.forEach(t => ignored.add(t));
return this;
Expand DownExpand Up@@ -222,6 +227,7 @@ export function createRunner(...paths: string[]) {
const expectedEnvelopeCount = expectedEnvelopes.length;

let envelopeCount = 0;
let unexpectedEnvelopeError: Error | undefined;
const envelopeWaiters: { expected: Expected; resolve: () => void; reject: (e: unknown) => void }[] = [];
const {
resolve: setWorkerPort,
Expand DownExpand Up@@ -297,6 +303,10 @@ export function createRunner(...paths: string[]) {

// no match found
if (matchIndex < 0) {
if (failOnUnexpected) {
unexpectedEnvelopeError ??= new Error('Received an unexpected envelope');
reject(unexpectedEnvelopeError);
}
return;
}

Expand DownExpand Up@@ -429,7 +439,10 @@ export function createRunner(...paths: string[]) {

return {
completed: async function (): Promise<void> {
return isComplete;
await isComplete;
if (unexpectedEnvelopeError) {
throw unexpectedEnvelopeError;
}
},
makeRequest: async function <T>(
method: 'get' | 'post',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
CACHE_DO: DurableObjectNamespace;
NO_CACHE_DO: DurableObjectNamespace;
}

/**
* Sync KV and SQL work against the DO's own storage, which the SDK instruments into `db` spans.
* Used to check that those spans still reach the transport from inside a Durable Object, where a
* cached client never hits an invocation-boundary flush and has to rely on the eager drain.
*/
function runStorageOps(ctx: DurableObjectState): { listSize: number; rows: number } {
ctx.storage.kv.put('cache-key', { hello: 'sync' });
ctx.storage.kv.get('cache-key');
const entries = [...ctx.storage.kv.list()];
ctx.storage.kv.delete('cache-key');

ctx.storage.sql.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
ctx.storage.sql.exec('INSERT INTO users (name) VALUES (?)', 'Alice');
const rows = ctx.storage.sql.exec('SELECT * FROM users').toArray();

return { listSize: entries.length, rows: rows.length };
}

function startDetachedWork(message: string): string {
void (async () => {
await new Promise(r => setTimeout(r, 3000));
await Sentry.startSpan({ name: 'do.detached-task', op: 'task' }, async () => {
Sentry.logger.info(`Detached log: ${message}`);
Sentry.metrics.count('do.detached', 1);
Sentry.captureException(new Error(message));
});
})();
return `Detached work started: ${message}`;
}

// DO with cacheClient: true (the default) — detached work events SHOULD be captured
class CacheDurableObjectBase extends DurableObject<Env> {
async echo(n: number): Promise<number> {
return n;
}

async handlerError(instanceId: string): Promise<void> {
throw new Error(`Cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async scopeCheck(seed: boolean): Promise<string> {
if (seed) {
Sentry.setTag('seeded_tag', 'from-seeding-call');
Sentry.setUser({ id: 'user-from-seeding-call' });
}
Sentry.captureException(new Error(seed ? 'Cache scope seed' : 'Cache scope probe'));
return 'ok';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from cache DO ${url.searchParams.get('id')}`));
}
if (url.pathname === '/streaming') {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('chunk1'));
controller.enqueue(new TextEncoder().encode('chunk2'));
controller.close();
},
});
return new Response(stream, { headers: { 'content-type': 'text/event-stream' } });
}
return new Response('Cache DO');
}
}

// DO with cacheClient: false — detached work events should NOT be captured
class NoCacheDurableObjectBase extends DurableObject<Env> {
async handlerError(instanceId: string): Promise<void> {
throw new Error(`No-cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `no-cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from no-cache DO ${url.searchParams.get('id')}`));
}
return new Response('No-cache DO');
}
}

export const CacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
CacheDurableObjectBase,
);

export const NoCacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
cacheClient: false,
enableRpcTracePropagation: true,
}),
NoCacheDurableObjectBase,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
{
async fetch(request, env, ctx) {
const url = new URL(request.url);
const instanceId = url.searchParams.get('id') || 'default';
const errorMessage = url.searchParams.get('errorMessage') || 'Same error';

// Work that finishes AFTER the response: a post-response span tree plus a
// log, metric and error, all registered via waitUntil. This is the worker-side
// half of the #22545 lifecycle (the DO-side half is /detached).
if (url.pathname === '/post-response') {
ctx.waitUntil(
Sentry.startSpan({ name: 'checkout.post-response', op: 'task' }, async () => {
Sentry.logger.info('checkout post-response log');
Sentry.metrics.count('checkout.processed', 1);
await new Promise(r => setTimeout(r, 50));
await Sentry.startSpan({ name: 'checkout.notify-webhook', op: 'http.client' }, async () => {
await new Promise(r => setTimeout(r, 25));
Sentry.captureException(new Error('Webhook delivery failed'));
});
}),
);
return new Response('checkout accepted');
}

// Fan a single request out into N sequential DO RPC calls — every RPC span must
// land in this request's trace when RPC trace propagation is on.
if (url.pathname === '/burst') {
const n = Math.min(Number(url.searchParams.get('n')) || 1, 20);
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`burst-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;

let sum = 0;
for (let i = 0; i < n; i++) {
sum += (await stub.echo(i)) as number;
}
return Response.json({ calls: n, sum });
}

// Cache DO RPC calls
if (url.pathname === '/cache/handler-error') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/cache/dedupe') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/cache/scope') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.scopeCheck(url.searchParams.get('seed') === '1'));
}

// Cache DO fetch calls — detached work goes through fetch (matching the #22545 repro),
// since the DO fetch handler always initializes the DO's own client
if (url.pathname === '/cache/detached') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

if (url.pathname === '/cache/storage') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/cache/streaming') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request('http://do/streaming'));
}

// No-cache DO calls
if (url.pathname === '/no-cache/handler-error') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/no-cache/dedupe') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/no-cache/storage') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/no-cache/detached') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

return new Response('Hello World!');
},
} satisfies ExportedHandler<Env>,
);
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
Merged
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
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -485,7 +485,7 @@ module.exports = [
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: false,
brotli: false,
limit: '522 KiB',
limit: '540 KiB',
disablePlugins: ['@size-limit/webpack'],
webpack: false,
modifyEsbuildConfig: function (config) {
Expand Down
15 changes: 14 additions & 1 deletion dev-packages/cloudflare-integration-tests/runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -161,6 +161,7 @@ export function createRunner(...paths: string[]) {

// controls whether envelopes are expected in predefined order or not
let unordered = false;
let failOnUnexpected = false;

if (!existsSync(testPath)) {
throw new Error(`Test scenario not found: ${testPath}`);
Expand DownExpand Up@@ -195,6 +196,10 @@ export function createRunner(...paths: string[]) {
unordered = true;
return this;
},
failOnUnexpected: function () {
failOnUnexpected = true;
return this;
},
ignore: function (...types: EnvelopeItemType[]) {
types.forEach(t => ignored.add(t));
return this;
Expand DownExpand Up@@ -222,6 +227,7 @@ export function createRunner(...paths: string[]) {
const expectedEnvelopeCount = expectedEnvelopes.length;

let envelopeCount = 0;
let unexpectedEnvelopeError: Error | undefined;
const envelopeWaiters: { expected: Expected; resolve: () => void; reject: (e: unknown) => void }[] = [];
const {
resolve: setWorkerPort,
Expand DownExpand Up@@ -297,6 +303,10 @@ export function createRunner(...paths: string[]) {

// no match found
if (matchIndex < 0) {
if (failOnUnexpected) {
unexpectedEnvelopeError ??= new Error('Received an unexpected envelope');
reject(unexpectedEnvelopeError);
}
return;
}

Expand DownExpand Up@@ -429,7 +439,10 @@ export function createRunner(...paths: string[]) {

return {
completed: async function (): Promise<void> {
return isComplete;
await isComplete;
if (unexpectedEnvelopeError) {
throw unexpectedEnvelopeError;
}
},
makeRequest: async function <T>(
method: 'get' | 'post',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
CACHE_DO: DurableObjectNamespace;
NO_CACHE_DO: DurableObjectNamespace;
}

/**
* Sync KV and SQL work against the DO's own storage, which the SDK instruments into `db` spans.
* Used to check that those spans still reach the transport from inside a Durable Object, where a
* cached client never hits an invocation-boundary flush and has to rely on the eager drain.
*/
function runStorageOps(ctx: DurableObjectState): { listSize: number; rows: number } {
ctx.storage.kv.put('cache-key', { hello: 'sync' });
ctx.storage.kv.get('cache-key');
const entries = [...ctx.storage.kv.list()];
ctx.storage.kv.delete('cache-key');

ctx.storage.sql.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
ctx.storage.sql.exec('INSERT INTO users (name) VALUES (?)', 'Alice');
const rows = ctx.storage.sql.exec('SELECT * FROM users').toArray();

return { listSize: entries.length, rows: rows.length };
}

function startDetachedWork(message: string): string {
void (async () => {
await new Promise(r => setTimeout(r, 3000));
await Sentry.startSpan({ name: 'do.detached-task', op: 'task' }, async () => {
Sentry.logger.info(`Detached log: ${message}`);
Sentry.metrics.count('do.detached', 1);
Sentry.captureException(new Error(message));
});
})();
return `Detached work started: ${message}`;
}

// DO with cacheClient: true (the default) — detached work events SHOULD be captured
class CacheDurableObjectBase extends DurableObject<Env> {
async echo(n: number): Promise<number> {
return n;
}

async handlerError(instanceId: string): Promise<void> {
throw new Error(`Cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async scopeCheck(seed: boolean): Promise<string> {
if (seed) {
Sentry.setTag('seeded_tag', 'from-seeding-call');
Sentry.setUser({ id: 'user-from-seeding-call' });
}
Sentry.captureException(new Error(seed ? 'Cache scope seed' : 'Cache scope probe'));
return 'ok';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from cache DO ${url.searchParams.get('id')}`));
}
if (url.pathname === '/streaming') {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('chunk1'));
controller.enqueue(new TextEncoder().encode('chunk2'));
controller.close();
},
});
return new Response(stream, { headers: { 'content-type': 'text/event-stream' } });
}
return new Response('Cache DO');
}
}

// DO with cacheClient: false — detached work events should NOT be captured
class NoCacheDurableObjectBase extends DurableObject<Env> {
async handlerError(instanceId: string): Promise<void> {
throw new Error(`No-cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `no-cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from no-cache DO ${url.searchParams.get('id')}`));
}
return new Response('No-cache DO');
}
}

export const CacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
CacheDurableObjectBase,
);

export const NoCacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
cacheClient: false,
enableRpcTracePropagation: true,
}),
NoCacheDurableObjectBase,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
{
async fetch(request, env, ctx) {
const url = new URL(request.url);
const instanceId = url.searchParams.get('id') || 'default';
const errorMessage = url.searchParams.get('errorMessage') || 'Same error';

// Work that finishes AFTER the response: a post-response span tree plus a
// log, metric and error, all registered via waitUntil. This is the worker-side
// half of the #22545 lifecycle (the DO-side half is /detached).
if (url.pathname === '/post-response') {
ctx.waitUntil(
Sentry.startSpan({ name: 'checkout.post-response', op: 'task' }, async () => {
Sentry.logger.info('checkout post-response log');
Sentry.metrics.count('checkout.processed', 1);
await new Promise(r => setTimeout(r, 50));
await Sentry.startSpan({ name: 'checkout.notify-webhook', op: 'http.client' }, async () => {
await new Promise(r => setTimeout(r, 25));
Sentry.captureException(new Error('Webhook delivery failed'));
});
}),
);
return new Response('checkout accepted');
}

// Fan a single request out into N sequential DO RPC calls — every RPC span must
// land in this request's trace when RPC trace propagation is on.
if (url.pathname === '/burst') {
const n = Math.min(Number(url.searchParams.get('n')) || 1, 20);
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`burst-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;

let sum = 0;
for (let i = 0; i < n; i++) {
sum += (await stub.echo(i)) as number;
}
return Response.json({ calls: n, sum });
}

// Cache DO RPC calls
if (url.pathname === '/cache/handler-error') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/cache/dedupe') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/cache/scope') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.scopeCheck(url.searchParams.get('seed') === '1'));
}

// Cache DO fetch calls — detached work goes through fetch (matching the #22545 repro),
// since the DO fetch handler always initializes the DO's own client
if (url.pathname === '/cache/detached') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

if (url.pathname === '/cache/storage') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/cache/streaming') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request('http://do/streaming'));
}

// No-cache DO calls
if (url.pathname === '/no-cache/handler-error') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/no-cache/dedupe') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/no-cache/storage') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/no-cache/detached') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

return new Response('Hello World!');
},
} satisfies ExportedHandler<Env>,
);
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
Merged
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
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -485,7 +485,7 @@ module.exports = [
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: false,
brotli: false,
limit: '522 KiB',
limit: '540 KiB',
disablePlugins: ['@size-limit/webpack'],
webpack: false,
modifyEsbuildConfig: function (config) {
Expand Down
15 changes: 14 additions & 1 deletion dev-packages/cloudflare-integration-tests/runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -161,6 +161,7 @@ export function createRunner(...paths: string[]) {

// controls whether envelopes are expected in predefined order or not
let unordered = false;
let failOnUnexpected = false;

if (!existsSync(testPath)) {
throw new Error(`Test scenario not found: ${testPath}`);
Expand DownExpand Up@@ -195,6 +196,10 @@ export function createRunner(...paths: string[]) {
unordered = true;
return this;
},
failOnUnexpected: function () {
failOnUnexpected = true;
return this;
},
ignore: function (...types: EnvelopeItemType[]) {
types.forEach(t => ignored.add(t));
return this;
Expand DownExpand Up@@ -222,6 +227,7 @@ export function createRunner(...paths: string[]) {
const expectedEnvelopeCount = expectedEnvelopes.length;

let envelopeCount = 0;
let unexpectedEnvelopeError: Error | undefined;
const envelopeWaiters: { expected: Expected; resolve: () => void; reject: (e: unknown) => void }[] = [];
const {
resolve: setWorkerPort,
Expand DownExpand Up@@ -297,6 +303,10 @@ export function createRunner(...paths: string[]) {

// no match found
if (matchIndex < 0) {
if (failOnUnexpected) {
unexpectedEnvelopeError ??= new Error('Received an unexpected envelope');
reject(unexpectedEnvelopeError);
}
return;
}

Expand DownExpand Up@@ -429,7 +439,10 @@ export function createRunner(...paths: string[]) {

return {
completed: async function (): Promise<void> {
return isComplete;
await isComplete;
if (unexpectedEnvelopeError) {
throw unexpectedEnvelopeError;
}
},
makeRequest: async function <T>(
method: 'get' | 'post',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
CACHE_DO: DurableObjectNamespace;
NO_CACHE_DO: DurableObjectNamespace;
}

/**
* Sync KV and SQL work against the DO's own storage, which the SDK instruments into `db` spans.
* Used to check that those spans still reach the transport from inside a Durable Object, where a
* cached client never hits an invocation-boundary flush and has to rely on the eager drain.
*/
function runStorageOps(ctx: DurableObjectState): { listSize: number; rows: number } {
ctx.storage.kv.put('cache-key', { hello: 'sync' });
ctx.storage.kv.get('cache-key');
const entries = [...ctx.storage.kv.list()];
ctx.storage.kv.delete('cache-key');

ctx.storage.sql.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
ctx.storage.sql.exec('INSERT INTO users (name) VALUES (?)', 'Alice');
const rows = ctx.storage.sql.exec('SELECT * FROM users').toArray();

return { listSize: entries.length, rows: rows.length };
}

function startDetachedWork(message: string): string {
void (async () => {
await new Promise(r => setTimeout(r, 3000));
await Sentry.startSpan({ name: 'do.detached-task', op: 'task' }, async () => {
Sentry.logger.info(`Detached log: ${message}`);
Sentry.metrics.count('do.detached', 1);
Sentry.captureException(new Error(message));
});
})();
return `Detached work started: ${message}`;
}

// DO with cacheClient: true (the default) — detached work events SHOULD be captured
class CacheDurableObjectBase extends DurableObject<Env> {
async echo(n: number): Promise<number> {
return n;
}

async handlerError(instanceId: string): Promise<void> {
throw new Error(`Cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async scopeCheck(seed: boolean): Promise<string> {
if (seed) {
Sentry.setTag('seeded_tag', 'from-seeding-call');
Sentry.setUser({ id: 'user-from-seeding-call' });
}
Sentry.captureException(new Error(seed ? 'Cache scope seed' : 'Cache scope probe'));
return 'ok';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from cache DO ${url.searchParams.get('id')}`));
}
if (url.pathname === '/streaming') {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('chunk1'));
controller.enqueue(new TextEncoder().encode('chunk2'));
controller.close();
},
});
return new Response(stream, { headers: { 'content-type': 'text/event-stream' } });
}
return new Response('Cache DO');
}
}

// DO with cacheClient: false — detached work events should NOT be captured
class NoCacheDurableObjectBase extends DurableObject<Env> {
async handlerError(instanceId: string): Promise<void> {
throw new Error(`No-cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `no-cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from no-cache DO ${url.searchParams.get('id')}`));
}
return new Response('No-cache DO');
}
}

export const CacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
CacheDurableObjectBase,
);

export const NoCacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
cacheClient: false,
enableRpcTracePropagation: true,
}),
NoCacheDurableObjectBase,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
{
async fetch(request, env, ctx) {
const url = new URL(request.url);
const instanceId = url.searchParams.get('id') || 'default';
const errorMessage = url.searchParams.get('errorMessage') || 'Same error';

// Work that finishes AFTER the response: a post-response span tree plus a
// log, metric and error, all registered via waitUntil. This is the worker-side
// half of the #22545 lifecycle (the DO-side half is /detached).
if (url.pathname === '/post-response') {
ctx.waitUntil(
Sentry.startSpan({ name: 'checkout.post-response', op: 'task' }, async () => {
Sentry.logger.info('checkout post-response log');
Sentry.metrics.count('checkout.processed', 1);
await new Promise(r => setTimeout(r, 50));
await Sentry.startSpan({ name: 'checkout.notify-webhook', op: 'http.client' }, async () => {
await new Promise(r => setTimeout(r, 25));
Sentry.captureException(new Error('Webhook delivery failed'));
});
}),
);
return new Response('checkout accepted');
}

// Fan a single request out into N sequential DO RPC calls — every RPC span must
// land in this request's trace when RPC trace propagation is on.
if (url.pathname === '/burst') {
const n = Math.min(Number(url.searchParams.get('n')) || 1, 20);
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`burst-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;

let sum = 0;
for (let i = 0; i < n; i++) {
sum += (await stub.echo(i)) as number;
}
return Response.json({ calls: n, sum });
}

// Cache DO RPC calls
if (url.pathname === '/cache/handler-error') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/cache/dedupe') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/cache/scope') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.scopeCheck(url.searchParams.get('seed') === '1'));
}

// Cache DO fetch calls — detached work goes through fetch (matching the #22545 repro),
// since the DO fetch handler always initializes the DO's own client
if (url.pathname === '/cache/detached') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

if (url.pathname === '/cache/storage') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/cache/streaming') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request('http://do/streaming'));
}

// No-cache DO calls
if (url.pathname === '/no-cache/handler-error') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/no-cache/dedupe') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/no-cache/storage') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/no-cache/detached') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

return new Response('Hello World!');
},
} satisfies ExportedHandler<Env>,
);
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
Merged
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
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -485,7 +485,7 @@ module.exports = [
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: false,
brotli: false,
limit: '522 KiB',
limit: '540 KiB',
disablePlugins: ['@size-limit/webpack'],
webpack: false,
modifyEsbuildConfig: function (config) {
Expand Down
15 changes: 14 additions & 1 deletion dev-packages/cloudflare-integration-tests/runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -161,6 +161,7 @@ export function createRunner(...paths: string[]) {

// controls whether envelopes are expected in predefined order or not
let unordered = false;
let failOnUnexpected = false;

if (!existsSync(testPath)) {
throw new Error(`Test scenario not found: ${testPath}`);
Expand DownExpand Up@@ -195,6 +196,10 @@ export function createRunner(...paths: string[]) {
unordered = true;
return this;
},
failOnUnexpected: function () {
failOnUnexpected = true;
return this;
},
ignore: function (...types: EnvelopeItemType[]) {
types.forEach(t => ignored.add(t));
return this;
Expand DownExpand Up@@ -222,6 +227,7 @@ export function createRunner(...paths: string[]) {
const expectedEnvelopeCount = expectedEnvelopes.length;

let envelopeCount = 0;
let unexpectedEnvelopeError: Error | undefined;
const envelopeWaiters: { expected: Expected; resolve: () => void; reject: (e: unknown) => void }[] = [];
const {
resolve: setWorkerPort,
Expand DownExpand Up@@ -297,6 +303,10 @@ export function createRunner(...paths: string[]) {

// no match found
if (matchIndex < 0) {
if (failOnUnexpected) {
unexpectedEnvelopeError ??= new Error('Received an unexpected envelope');
reject(unexpectedEnvelopeError);
}
return;
}

Expand DownExpand Up@@ -429,7 +439,10 @@ export function createRunner(...paths: string[]) {

return {
completed: async function (): Promise<void> {
return isComplete;
await isComplete;
if (unexpectedEnvelopeError) {
throw unexpectedEnvelopeError;
}
},
makeRequest: async function <T>(
method: 'get' | 'post',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
CACHE_DO: DurableObjectNamespace;
NO_CACHE_DO: DurableObjectNamespace;
}

/**
* Sync KV and SQL work against the DO's own storage, which the SDK instruments into `db` spans.
* Used to check that those spans still reach the transport from inside a Durable Object, where a
* cached client never hits an invocation-boundary flush and has to rely on the eager drain.
*/
function runStorageOps(ctx: DurableObjectState): { listSize: number; rows: number } {
ctx.storage.kv.put('cache-key', { hello: 'sync' });
ctx.storage.kv.get('cache-key');
const entries = [...ctx.storage.kv.list()];
ctx.storage.kv.delete('cache-key');

ctx.storage.sql.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
ctx.storage.sql.exec('INSERT INTO users (name) VALUES (?)', 'Alice');
const rows = ctx.storage.sql.exec('SELECT * FROM users').toArray();

return { listSize: entries.length, rows: rows.length };
}

function startDetachedWork(message: string): string {
void (async () => {
await new Promise(r => setTimeout(r, 3000));
await Sentry.startSpan({ name: 'do.detached-task', op: 'task' }, async () => {
Sentry.logger.info(`Detached log: ${message}`);
Sentry.metrics.count('do.detached', 1);
Sentry.captureException(new Error(message));
});
})();
return `Detached work started: ${message}`;
}

// DO with cacheClient: true (the default) — detached work events SHOULD be captured
class CacheDurableObjectBase extends DurableObject<Env> {
async echo(n: number): Promise<number> {
return n;
}

async handlerError(instanceId: string): Promise<void> {
throw new Error(`Cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async scopeCheck(seed: boolean): Promise<string> {
if (seed) {
Sentry.setTag('seeded_tag', 'from-seeding-call');
Sentry.setUser({ id: 'user-from-seeding-call' });
}
Sentry.captureException(new Error(seed ? 'Cache scope seed' : 'Cache scope probe'));
return 'ok';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from cache DO ${url.searchParams.get('id')}`));
}
if (url.pathname === '/streaming') {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('chunk1'));
controller.enqueue(new TextEncoder().encode('chunk2'));
controller.close();
},
});
return new Response(stream, { headers: { 'content-type': 'text/event-stream' } });
}
return new Response('Cache DO');
}
}

// DO with cacheClient: false — detached work events should NOT be captured
class NoCacheDurableObjectBase extends DurableObject<Env> {
async handlerError(instanceId: string): Promise<void> {
throw new Error(`No-cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `no-cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from no-cache DO ${url.searchParams.get('id')}`));
}
return new Response('No-cache DO');
}
}

export const CacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
CacheDurableObjectBase,
);

export const NoCacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
cacheClient: false,
enableRpcTracePropagation: true,
}),
NoCacheDurableObjectBase,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
{
async fetch(request, env, ctx) {
const url = new URL(request.url);
const instanceId = url.searchParams.get('id') || 'default';
const errorMessage = url.searchParams.get('errorMessage') || 'Same error';

// Work that finishes AFTER the response: a post-response span tree plus a
// log, metric and error, all registered via waitUntil. This is the worker-side
// half of the #22545 lifecycle (the DO-side half is /detached).
if (url.pathname === '/post-response') {
ctx.waitUntil(
Sentry.startSpan({ name: 'checkout.post-response', op: 'task' }, async () => {
Sentry.logger.info('checkout post-response log');
Sentry.metrics.count('checkout.processed', 1);
await new Promise(r => setTimeout(r, 50));
await Sentry.startSpan({ name: 'checkout.notify-webhook', op: 'http.client' }, async () => {
await new Promise(r => setTimeout(r, 25));
Sentry.captureException(new Error('Webhook delivery failed'));
});
}),
);
return new Response('checkout accepted');
}

// Fan a single request out into N sequential DO RPC calls — every RPC span must
// land in this request's trace when RPC trace propagation is on.
if (url.pathname === '/burst') {
const n = Math.min(Number(url.searchParams.get('n')) || 1, 20);
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`burst-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;

let sum = 0;
for (let i = 0; i < n; i++) {
sum += (await stub.echo(i)) as number;
}
return Response.json({ calls: n, sum });
}

// Cache DO RPC calls
if (url.pathname === '/cache/handler-error') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/cache/dedupe') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/cache/scope') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.scopeCheck(url.searchParams.get('seed') === '1'));
}

// Cache DO fetch calls — detached work goes through fetch (matching the #22545 repro),
// since the DO fetch handler always initializes the DO's own client
if (url.pathname === '/cache/detached') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

if (url.pathname === '/cache/storage') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/cache/streaming') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request('http://do/streaming'));
}

// No-cache DO calls
if (url.pathname === '/no-cache/handler-error') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/no-cache/dedupe') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/no-cache/storage') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/no-cache/detached') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

return new Response('Hello World!');
},
} satisfies ExportedHandler<Env>,
);
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
Merged
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
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -485,7 +485,7 @@ module.exports = [
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: false,
brotli: false,
limit: '522 KiB',
limit: '540 KiB',
disablePlugins: ['@size-limit/webpack'],
webpack: false,
modifyEsbuildConfig: function (config) {
Expand Down
15 changes: 14 additions & 1 deletion dev-packages/cloudflare-integration-tests/runner.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -161,6 +161,7 @@ export function createRunner(...paths: string[]) {

// controls whether envelopes are expected in predefined order or not
let unordered = false;
let failOnUnexpected = false;

if (!existsSync(testPath)) {
throw new Error(`Test scenario not found: ${testPath}`);
Expand DownExpand Up@@ -195,6 +196,10 @@ export function createRunner(...paths: string[]) {
unordered = true;
return this;
},
failOnUnexpected: function () {
failOnUnexpected = true;
return this;
},
ignore: function (...types: EnvelopeItemType[]) {
types.forEach(t => ignored.add(t));
return this;
Expand DownExpand Up@@ -222,6 +227,7 @@ export function createRunner(...paths: string[]) {
const expectedEnvelopeCount = expectedEnvelopes.length;

let envelopeCount = 0;
let unexpectedEnvelopeError: Error | undefined;
const envelopeWaiters: { expected: Expected; resolve: () => void; reject: (e: unknown) => void }[] = [];
const {
resolve: setWorkerPort,
Expand DownExpand Up@@ -297,6 +303,10 @@ export function createRunner(...paths: string[]) {

// no match found
if (matchIndex < 0) {
if (failOnUnexpected) {
unexpectedEnvelopeError ??= new Error('Received an unexpected envelope');
reject(unexpectedEnvelopeError);
}
return;
}

Expand DownExpand Up@@ -429,7 +439,10 @@ export function createRunner(...paths: string[]) {

return {
completed: async function (): Promise<void> {
return isComplete;
await isComplete;
if (unexpectedEnvelopeError) {
throw unexpectedEnvelopeError;
}
},
makeRequest: async function <T>(
method: 'get' | 'post',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
CACHE_DO: DurableObjectNamespace;
NO_CACHE_DO: DurableObjectNamespace;
}

/**
* Sync KV and SQL work against the DO's own storage, which the SDK instruments into `db` spans.
* Used to check that those spans still reach the transport from inside a Durable Object, where a
* cached client never hits an invocation-boundary flush and has to rely on the eager drain.
*/
function runStorageOps(ctx: DurableObjectState): { listSize: number; rows: number } {
ctx.storage.kv.put('cache-key', { hello: 'sync' });
ctx.storage.kv.get('cache-key');
const entries = [...ctx.storage.kv.list()];
ctx.storage.kv.delete('cache-key');

ctx.storage.sql.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
ctx.storage.sql.exec('INSERT INTO users (name) VALUES (?)', 'Alice');
const rows = ctx.storage.sql.exec('SELECT * FROM users').toArray();

return { listSize: entries.length, rows: rows.length };
}

function startDetachedWork(message: string): string {
void (async () => {
await new Promise(r => setTimeout(r, 3000));
await Sentry.startSpan({ name: 'do.detached-task', op: 'task' }, async () => {
Sentry.logger.info(`Detached log: ${message}`);
Sentry.metrics.count('do.detached', 1);
Sentry.captureException(new Error(message));
});
})();
return `Detached work started: ${message}`;
}

// DO with cacheClient: true (the default) — detached work events SHOULD be captured
class CacheDurableObjectBase extends DurableObject<Env> {
async echo(n: number): Promise<number> {
return n;
}

async handlerError(instanceId: string): Promise<void> {
throw new Error(`Cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async scopeCheck(seed: boolean): Promise<string> {
if (seed) {
Sentry.setTag('seeded_tag', 'from-seeding-call');
Sentry.setUser({ id: 'user-from-seeding-call' });
}
Sentry.captureException(new Error(seed ? 'Cache scope seed' : 'Cache scope probe'));
return 'ok';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from cache DO ${url.searchParams.get('id')}`));
}
if (url.pathname === '/streaming') {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('chunk1'));
controller.enqueue(new TextEncoder().encode('chunk2'));
controller.close();
},
});
return new Response(stream, { headers: { 'content-type': 'text/event-stream' } });
}
return new Response('Cache DO');
}
}

// DO with cacheClient: false — detached work events should NOT be captured
class NoCacheDurableObjectBase extends DurableObject<Env> {
async handlerError(instanceId: string): Promise<void> {
throw new Error(`No-cache DO handler error from ${instanceId}`);
}

async dedupe(errorMessage: string): Promise<string> {
Sentry.captureException(new Error(errorMessage));
return 'dedupe test';
}

async storage(): Promise<string> {
const { listSize, rows } = runStorageOps(this.ctx);
return `no-cache storage ${listSize}/${rows}`;
}

async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/detached') {
return new Response(startDetachedWork(`Detached work from no-cache DO ${url.searchParams.get('id')}`));
}
return new Response('No-cache DO');
}
}

export const CacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
CacheDurableObjectBase,
);

export const NoCacheDurableObject = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
cacheClient: false,
enableRpcTracePropagation: true,
}),
NoCacheDurableObjectBase,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
{
async fetch(request, env, ctx) {
const url = new URL(request.url);
const instanceId = url.searchParams.get('id') || 'default';
const errorMessage = url.searchParams.get('errorMessage') || 'Same error';

// Work that finishes AFTER the response: a post-response span tree plus a
// log, metric and error, all registered via waitUntil. This is the worker-side
// half of the #22545 lifecycle (the DO-side half is /detached).
if (url.pathname === '/post-response') {
ctx.waitUntil(
Sentry.startSpan({ name: 'checkout.post-response', op: 'task' }, async () => {
Sentry.logger.info('checkout post-response log');
Sentry.metrics.count('checkout.processed', 1);
await new Promise(r => setTimeout(r, 50));
await Sentry.startSpan({ name: 'checkout.notify-webhook', op: 'http.client' }, async () => {
await new Promise(r => setTimeout(r, 25));
Sentry.captureException(new Error('Webhook delivery failed'));
});
}),
);
return new Response('checkout accepted');
}

// Fan a single request out into N sequential DO RPC calls — every RPC span must
// land in this request's trace when RPC trace propagation is on.
if (url.pathname === '/burst') {
const n = Math.min(Number(url.searchParams.get('n')) || 1, 20);
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`burst-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;

let sum = 0;
for (let i = 0; i < n; i++) {
sum += (await stub.echo(i)) as number;
}
return Response.json({ calls: n, sum });
}

// Cache DO RPC calls
if (url.pathname === '/cache/handler-error') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/cache/dedupe') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/cache/scope') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.scopeCheck(url.searchParams.get('seed') === '1'));
}

// Cache DO fetch calls — detached work goes through fetch (matching the #22545 repro),
// since the DO fetch handler always initializes the DO's own client
if (url.pathname === '/cache/detached') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

if (url.pathname === '/cache/storage') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/cache/streaming') {
const stub = env.CACHE_DO.get(
env.CACHE_DO.idFromName(`cache-do-${instanceId}`),
) as DurableObjectStub<CacheDurableObjectBase>;
return stub.fetch(new Request('http://do/streaming'));
}

// No-cache DO calls
if (url.pathname === '/no-cache/handler-error') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
await stub.handlerError(instanceId);
}

if (url.pathname === '/no-cache/dedupe') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
const result = await stub.dedupe(errorMessage);
return new Response(String(result));
}

if (url.pathname === '/no-cache/storage') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return new Response(await stub.storage());
}

if (url.pathname === '/no-cache/detached') {
const stub = env.NO_CACHE_DO.get(
env.NO_CACHE_DO.idFromName(`no-cache-do-${instanceId}`),
) as DurableObjectStub<NoCacheDurableObjectBase>;
return stub.fetch(new Request(`http://do/detached?id=${instanceId}`));
}

return new Response('Hello World!');
},
} satisfies ExportedHandler<Env>,
);
Loading
Loading