diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm/index.ts new file mode 100644 index 000000000000..f04a0ad37714 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm/index.ts @@ -0,0 +1,50 @@ +import * as Sentry from '@sentry/cloudflare'; +import { DurableObject } from 'cloudflare:workers'; + +interface Env { + SENTRY_DSN: string; + TEST_DURABLE_OBJECT: DurableObjectNamespace; +} + +class TestDurableObjectBase extends DurableObject { + public constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + } + + async alarm(): Promise { + const action = await this.ctx.storage.get('alarm-action'); + if (action === 'throw') { + throw new Error('Alarm error captured by Sentry'); + } + } + + async setAlarm(action: string): Promise { + await this.ctx.storage.put('alarm-action', action); + await this.ctx.storage.setAlarm(Date.now() + 500); + } +} + +export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry( + (env: Env) => ({ + dsn: env.SENTRY_DSN, + tracesSampleRate: 1.0, + instrumentPrototypeMethods: true, + }), + TestDurableObjectBase, +); + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const id: DurableObjectId = env.TEST_DURABLE_OBJECT.idFromName('test'); + const stub = env.TEST_DURABLE_OBJECT.get(id) as unknown as TestDurableObjectBase; + + if (url.pathname === '/setAlarm') { + const action = url.searchParams.get('action') || 'succeed'; + await stub.setAlarm(action); + return new Response('Alarm set'); + } + + return new Response('OK'); + }, +}; diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm/test.ts new file mode 100644 index 000000000000..ed3b44bba9d6 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm/test.ts @@ -0,0 +1,82 @@ +import { expect, it, describe } from 'vitest'; +import { createRunner } from '../../../runner'; + +describe('Alarm instrumentation', () => { + it('captures error from alarm handler', async ({ signal }) => { + let setAlarmTraceId: string | undefined; + + const runner = createRunner(__dirname) + .unignore('event') + // First envelope: transaction from setAlarm call + .expect(envelope => { + const transactionEvent = envelope[1]?.[0]?.[1]; + setAlarmTraceId = transactionEvent?.contexts?.trace?.trace_id; + }) + // Second envelope: error from alarm handler + .expect(envelope => { + const errorEvent = envelope[1]?.[0]?.[1]; + expect(errorEvent).toEqual( + expect.objectContaining({ + exception: expect.objectContaining({ + values: expect.arrayContaining([ + expect.objectContaining({ + value: 'Alarm error captured by Sentry', + mechanism: expect.objectContaining({ + type: 'auto.faas.cloudflare.durable_object', + }), + }), + ]), + }), + }), + ); + }) + .start(signal); + + await runner.makeRequest('get', '/setAlarm?action=throw'); + await runner.completed(); + + expect(setAlarmTraceId).toBeDefined(); + }); + + it('creates a transaction for alarm with new trace linked to setAlarm', async ({ signal }) => { + let setAlarmTraceId: string | undefined; + + const runner = createRunner(__dirname) + // First envelope: transaction from setAlarm call + .expect(envelope => { + const transactionEvent = envelope[1]?.[0]?.[1]; + setAlarmTraceId = transactionEvent?.contexts?.trace?.trace_id; + expect(setAlarmTraceId).toBeDefined(); + }) + // Second envelope: transaction from alarm handler + .expect(envelope => { + const alarmTransaction = envelope[1]?.[0]?.[1]; + + // Alarm creates a transaction with correct attributes + expect(alarmTransaction).toEqual( + expect.objectContaining({ + transaction: 'alarm', + contexts: expect.objectContaining({ + trace: expect.objectContaining({ + op: 'function', + origin: 'auto.faas.cloudflare.durable_object', + }), + }), + }), + ); + + // Alarm starts a new trace (different trace ID from the request that called setAlarm) + const alarmTraceId = alarmTransaction?.contexts?.trace?.trace_id; + expect(alarmTraceId).not.toBe(setAlarmTraceId); + + // Alarm links to the trace that called setAlarm via sentry.previous_trace attribute + const previousTrace = alarmTransaction?.contexts?.trace?.data?.['sentry.previous_trace']; + expect(previousTrace).toBeDefined(); + expect(previousTrace).toContain(setAlarmTraceId); + }) + .start(signal); + + await runner.makeRequest('get', '/setAlarm'); + await runner.completed(); + }); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm/wrangler.jsonc new file mode 100644 index 000000000000..8a544e1bdf6b --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm/wrangler.jsonc @@ -0,0 +1,20 @@ +{ + "name": "worker-name", + "main": "index.ts", + "compatibility_date": "2025-06-17", + "migrations": [ + { + "new_sqlite_classes": ["TestDurableObject"], + "tag": "v1", + }, + ], + "durable_objects": { + "bindings": [ + { + "class_name": "TestDurableObject", + "name": "TEST_DURABLE_OBJECT", + }, + ], + }, + "compatibility_flags": ["nodejs_als"], +} diff --git a/packages/cloudflare/src/client.ts b/packages/cloudflare/src/client.ts index 957cb7274d8e..7b7f46a90a50 100644 --- a/packages/cloudflare/src/client.ts +++ b/packages/cloudflare/src/client.ts @@ -1,7 +1,5 @@ import type { ClientOptions, Options, ServerRuntimeClientOptions } from '@sentry/core'; -import { applySdkMetadata, debug, ServerRuntimeClient } from '@sentry/core'; -import { DEBUG_BUILD } from './debug-build'; -import type { makeFlushLock } from './flush'; +import { applySdkMetadata, ServerRuntimeClient } from '@sentry/core'; import type { CloudflareTransportOptions } from './transport'; /** @@ -11,14 +9,6 @@ import type { CloudflareTransportOptions } from './transport'; * @see ServerRuntimeClient for usage documentation. */ export class CloudflareClient extends ServerRuntimeClient { - private readonly _flushLock: ReturnType | void; - private _pendingSpans: Set = new Set(); - private _spanCompletionPromise: Promise | null = null; - private _resolveSpanCompletion: (() => void) | null = null; - - private _unsubscribeSpanStart: (() => void) | null = null; - private _unsubscribeSpanEnd: (() => void) | null = null; - /** * Creates a new Cloudflare SDK instance. * @param options Configuration options for this SDK. @@ -26,10 +16,9 @@ export class CloudflareClient extends ServerRuntimeClient { public constructor(options: CloudflareClientOptions) { applySdkMetadata(options, 'cloudflare'); options._metadata = options._metadata || {}; - const { flushLock, ...serverOptions } = options; const clientOptions: ServerRuntimeClientOptions = { - ...serverOptions, + ...options, platform: 'javascript', // TODO: Grab version information runtime: { name: 'cloudflare' }, @@ -37,105 +26,22 @@ export class CloudflareClient extends ServerRuntimeClient { }; super(clientOptions); - this._flushLock = flushLock; - - // Track span lifecycle to know when to flush - this._unsubscribeSpanStart = this.on('spanStart', span => { - const spanId = span.spanContext().spanId; - DEBUG_BUILD && debug.log('[CloudflareClient] Span started:', spanId); - this._pendingSpans.add(spanId); - - if (!this._spanCompletionPromise) { - this._spanCompletionPromise = new Promise(resolve => { - this._resolveSpanCompletion = resolve; - }); - } - }); - - this._unsubscribeSpanEnd = this.on('spanEnd', span => { - const spanId = span.spanContext().spanId; - DEBUG_BUILD && debug.log('[CloudflareClient] Span ended:', spanId); - this._pendingSpans.delete(spanId); - - // If no more pending spans, resolve the completion promise - if (this._pendingSpans.size === 0 && this._resolveSpanCompletion) { - DEBUG_BUILD && debug.log('[CloudflareClient] All spans completed, resolving promise'); - this._resolveSpanCompletion(); - this._resetSpanCompletionPromise(); - } - }); - } - - /** - * Flushes pending operations and ensures all data is processed. - * If a timeout is provided, the operation will be completed within the specified time limit. - * - * It will wait for all pending spans to complete before flushing. - * - * @param {number} [timeout] - Optional timeout in milliseconds to force the completion of the flush operation. - * @return {Promise} A promise that resolves to a boolean indicating whether the flush operation was successful. - */ - public async flush(timeout?: number): Promise { - if (this._flushLock) { - await this._flushLock.finalize(); - } - - if (this._pendingSpans.size > 0 && this._spanCompletionPromise) { - DEBUG_BUILD && - debug.log('[CloudflareClient] Waiting for', this._pendingSpans.size, 'pending spans to complete...'); - - const timeoutMs = timeout ?? 5000; - const spanCompletionRace = Promise.race([ - this._spanCompletionPromise, - new Promise(resolve => - setTimeout(() => { - DEBUG_BUILD && - debug.log('[CloudflareClient] Span completion timeout after', timeoutMs, 'ms, flushing anyway'); - resolve(undefined); - }, timeoutMs), - ), - ]); - - await spanCompletionRace; - } - - return super.flush(timeout); } /** - * Disposes of the client and releases all resources. + * Drain the client's promise buffer instead of polling `_numProcessing`. * - * This method clears all Cloudflare-specific state in addition to the base client cleanup. - * It unsubscribes from span lifecycle events and clears pending span tracking. + * The base class polls `_numProcessing` every 1ms until it reaches 0. + * With a shared client under sustained load, `_numProcessing` never reaches 0 + * because new events keep being queued from other concurrent requests, causing + * the polling loop to always hit the full timeout (2s by default). * - * Call this method after flushing to allow the client to be garbage collected. - * After calling dispose(), the client should not be used anymore. - */ - public override dispose(): void { - DEBUG_BUILD && debug.log('[CloudflareClient] Disposing client...'); - - super.dispose(); - - if (this._unsubscribeSpanStart) { - this._unsubscribeSpanStart(); - this._unsubscribeSpanStart = null; - } - if (this._unsubscribeSpanEnd) { - this._unsubscribeSpanEnd(); - this._unsubscribeSpanEnd = null; - } - - this._resetSpanCompletionPromise(); - (this as unknown as { _flushLock: ReturnType | void })._flushLock = undefined; - } - - /** - * Resets the span completion promise and resolve function. + * `_promiseBuffer.drain()` snapshots the current buffer and waits for those + * specific tasks to complete via `Promise.allSettled`, without being blocked + * by tasks added later from other concurrent requests. */ - private _resetSpanCompletionPromise(): void { - this._pendingSpans.clear(); - this._spanCompletionPromise = null; - this._resolveSpanCompletion = null; + protected override _isClientDoneProcessing(timeout?: number): Promise { + return this._promiseBuffer.drain(timeout) as Promise; } } @@ -198,6 +104,4 @@ export interface CloudflareOptions extends Options, * * @see CloudflareClient for more information. */ -export interface CloudflareClientOptions extends ClientOptions, BaseCloudflareOptions { - flushLock?: ReturnType; -} +export interface CloudflareClientOptions extends ClientOptions, BaseCloudflareOptions {} diff --git a/packages/cloudflare/src/flush.ts b/packages/cloudflare/src/flush.ts index e7f036971f4a..08ac44383781 100644 --- a/packages/cloudflare/src/flush.ts +++ b/packages/cloudflare/src/flush.ts @@ -1,51 +1,14 @@ -import type { ExecutionContext } from '@cloudflare/workers-types'; import type { Client } from '@sentry/core'; import { flush } from '@sentry/core'; -type FlushLock = { - readonly ready: Promise; - readonly finalize: () => Promise; -}; - /** - * Enhances the given execution context by wrapping its `waitUntil` method with a proxy - * to monitor pending tasks, and provides a flusher function to ensure all tasks - * have been completed before executing any subsequent logic. + * Flushes the client to ensure all pending events are sent. * - * @param {ExecutionContext} context - The execution context to be enhanced. If no context is provided, the function returns undefined. - * @return {FlushLock} Returns a flusher function if a valid context is provided, otherwise undefined. - */ -export function makeFlushLock(context: ExecutionContext): FlushLock { - let resolveAllDone: () => void = () => undefined; - const allDone = new Promise(res => { - resolveAllDone = res; - }); - let pending = 0; - const originalWaitUntil = context.waitUntil.bind(context) as typeof context.waitUntil; - context.waitUntil = promise => { - pending++; - return originalWaitUntil( - promise.finally(() => { - if (--pending === 0) resolveAllDone(); - }), - ); - }; - return Object.freeze({ - ready: allDone, - finalize: () => { - if (pending === 0) resolveAllDone(); - return allDone; - }, - }); -} - -/** - * Flushes the client and then disposes of it to allow garbage collection. - * This should be called at the end of each request to prevent memory leaks. + * Note: The client is reused across requests, so we only flush without disposing. * - * @param client - The CloudflareClient instance to flush and dispose + * @param client - The CloudflareClient instance to flush * @param timeout - Timeout in milliseconds for the flush operation - * @returns A promise that resolves when flush and dispose are complete + * @returns A promise that resolves when flush is complete */ export async function flushAndDispose(client: Client | undefined, timeout = 2000): Promise { if (!client) { @@ -55,5 +18,4 @@ export async function flushAndDispose(client: Client | undefined, timeout = 2000 } await client.flush(timeout); - client.dispose(); } diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentEmail.ts b/packages/cloudflare/src/instrumentations/worker/instrumentEmail.ts index a557bdcb164d..782fb6ecb7c5 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentEmail.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentEmail.ts @@ -28,7 +28,7 @@ function wrapEmailHandler( return withIsolationScope(isolationScope => { const waitUntil = context.waitUntil.bind(context); - const client = init({ ...options, ctx: context }); + const client = init(options); isolationScope.setClient(client); addCloudResourceContext(isolationScope); diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts b/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts index 0f7df2e27adc..813dc1c32bf4 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts @@ -29,7 +29,7 @@ function wrapQueueHandler( return withIsolationScope(isolationScope => { const waitUntil = context.waitUntil.bind(context); - const client = init({ ...options, ctx: context }); + const client = init(options); isolationScope.setClient(client); addCloudResourceContext(isolationScope); diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentScheduled.ts b/packages/cloudflare/src/instrumentations/worker/instrumentScheduled.ts index 455acab2b0dd..ec0c92298f30 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentScheduled.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentScheduled.ts @@ -25,7 +25,7 @@ function wrapScheduledHandler( return withIsolationScope(isolationScope => { const waitUntil = context.waitUntil.bind(context); - const client = init({ ...options, ctx: context }); + const client = init(options); isolationScope.setClient(client); addCloudResourceContext(isolationScope); diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentTail.ts b/packages/cloudflare/src/instrumentations/worker/instrumentTail.ts index 283a238053c0..d04beb5d383e 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentTail.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentTail.ts @@ -18,7 +18,7 @@ function wrapTailHandler(options: CloudflareOptions, context: ExecutionContext, return withIsolationScope(async isolationScope => { const waitUntil = context.waitUntil.bind(context); - const client = init({ ...options, ctx: context }); + const client = init(options); isolationScope.setClient(client); addCloudResourceContext(isolationScope); diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 05e870ff5d81..2f7ae8006164 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -2,11 +2,11 @@ import type { CfProperties, ExecutionContext, IncomingRequestCfProperties } from import { captureException, continueTrace, - getClient, getHttpSpanDetailsFromUrlObject, httpHeadersToSpanAttributes, parseStringToURLObject, SEMANTIC_ATTRIBUTE_SENTRY_OP, + setCurrentClient, setHttpStatus, startSpanManual, winterCGHeadersToDict, @@ -48,8 +48,12 @@ export function wrapRequestHandler( const waitUntil = context?.waitUntil?.bind?.(context); - const client = init({ ...options, ctx: context }); + const client = init(options); isolationScope.setClient(client); + // Also set on current scope so getClient() works in nested wrappers + if (client) { + setCurrentClient(client); + } const urlObject = parseStringToURLObject(request.url); const [name, attributes] = getHttpSpanDetailsFromUrlObject(urlObject, 'server', 'auto.http.cloudflare', request); @@ -68,7 +72,7 @@ export function wrapRequestHandler( attributes, httpHeadersToSpanAttributes( winterCGHeadersToDict(request.headers), - getClient()?.getOptions().sendDefaultPii ?? false, + client?.getOptions()?.sendDefaultPii ?? false, ), ); @@ -176,7 +180,10 @@ export function wrapRequestHandler( // This includes WebSocket upgrades where webSocketMessage/webSocketClose handlers // will still be called and may need the client to capture errors. if (res.status === 101) { - waitUntil?.(client?.flush(2000)); + const flushPromise = client?.flush(2000); + if (flushPromise) { + waitUntil?.(flushPromise); + } } else { waitUntil?.(flushAndDispose(client)); } diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts index a5eb7f4edcda..d73a0d1370c8 100644 --- a/packages/cloudflare/src/sdk.ts +++ b/packages/cloudflare/src/sdk.ts @@ -4,17 +4,18 @@ import { conversationIdIntegration, dedupeIntegration, functionToStringIntegration, + getGlobalScope, getIntegrationsToSetup, inboundFiltersIntegration, initAndBind, linkedErrorsIntegration, requestDataIntegration, + setCurrentClient, spanStreamingIntegration, stackParserFromStackParserOptions, } from '@sentry/core'; import type { CloudflareClientOptions, CloudflareOptions } from './client'; import { CloudflareClient } from './client'; -import { makeFlushLock } from './flush'; import { fetchIntegration } from './integrations/fetch'; import { honoIntegration } from './integrations/hono'; import { setupOpenTelemetryTracer } from './opentelemetry/tracer'; @@ -44,13 +45,22 @@ export function getDefaultIntegrations(options: CloudflareOptions): Integration[ /** * Initializes the cloudflare SDK. + * + * If a client already exists on the global scope, it will be reused and bound to the current scope. */ export function init(options: CloudflareOptions): CloudflareClient | undefined { + // Reuse existing client from global scope - just bind it to the current scope + const existingClient = getGlobalScope().getClient(); + if (existingClient) { + setCurrentClient(existingClient); + return existingClient; + } + if (options.defaultIntegrations === undefined) { options.defaultIntegrations = getDefaultIntegrations(options); } - const flushLock = options.ctx ? makeFlushLock(options.ctx) : undefined; + // ctx is no longer used for client creation - flush is handled per-request delete options.ctx; const resolvedIntegrations = getIntegrationsToSetup(options); @@ -63,7 +73,6 @@ export function init(options: CloudflareOptions): CloudflareClient | undefined { stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), integrations: resolvedIntegrations, transport: options.transport || makeCloudflareTransport, - flushLock, }; /** @@ -77,5 +86,8 @@ export function init(options: CloudflareOptions): CloudflareClient | undefined { setupOpenTelemetryTracer(); } - return initAndBind(CloudflareClient, clientOptions) as CloudflareClient; + const client = initAndBind(CloudflareClient, clientOptions) as CloudflareClient; + // Also set on global scope for reuse across requests + getGlobalScope().setClient(client); + return client; } diff --git a/packages/cloudflare/src/wrapMethodWithSentry.ts b/packages/cloudflare/src/wrapMethodWithSentry.ts index 3a7218057c4a..8d9a09e6b850 100644 --- a/packages/cloudflare/src/wrapMethodWithSentry.ts +++ b/packages/cloudflare/src/wrapMethodWithSentry.ts @@ -87,22 +87,19 @@ export function wrapMethodWithSentry( // but the scope still holds a reference to it (e.g., alarm handlers in Durable Objects) // For startNewTrace, always create a fresh client if (startNewTrace || !scopeClient?.getTransport()) { - const client = init({ - ...wrapperOptions.options, - ctx: context as unknown as ExecutionContext | undefined, - }); + const client = init(wrapperOptions.options); scope.setClient(client); scopeClient = client; } - const clientToDispose = scopeClient; + const clientToFlush = scopeClient; const methodName = wrapperOptions.spanName || 'unknown'; const teardown = async (): Promise => { if (startNewTrace && storage) { await storeSpanContext(storage, methodName); } - await flushAndDispose(clientToDispose); + await flushAndDispose(clientToFlush); }; if (!wrapperOptions.spanName) { diff --git a/packages/cloudflare/test/client.test.ts b/packages/cloudflare/test/client.test.ts index ecf85c0f4c2c..a9f130893831 100644 --- a/packages/cloudflare/test/client.test.ts +++ b/packages/cloudflare/test/client.test.ts @@ -1,7 +1,6 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { setAsyncLocalStorageAsyncContextStrategy } from '../src/async'; import { CloudflareClient, type CloudflareClientOptions } from '../src/client'; -import { makeFlushLock } from '../src/flush'; const MOCK_CLIENT_OPTIONS: CloudflareClientOptions = { dsn: 'https://public@dsn.ingest.sentry.io/1337', @@ -23,75 +22,9 @@ describe('CloudflareClient', () => { }); describe('dispose()', () => { - it('unsubscribes from span lifecycle events', () => { - const client = new CloudflareClient(MOCK_CLIENT_OPTIONS); - - // Access the private unsubscribe functions to verify they exist - const privateClient = client as unknown as { - _unsubscribeSpanStart: (() => void) | null; - _unsubscribeSpanEnd: (() => void) | null; - }; - - expect(privateClient._unsubscribeSpanStart).not.toBeNull(); - expect(privateClient._unsubscribeSpanEnd).not.toBeNull(); - - client.dispose(); - - expect(privateClient._unsubscribeSpanStart).toBeNull(); - expect(privateClient._unsubscribeSpanEnd).toBeNull(); - }); - - it('clears pending spans tracking', () => { - const client = new CloudflareClient(MOCK_CLIENT_OPTIONS); - - const privateClient = client as unknown as { - _pendingSpans: Set; - _spanCompletionPromise: Promise | null; - _resolveSpanCompletion: (() => void) | null; - }; - - // Add some pending spans - privateClient._pendingSpans.add('span1'); - privateClient._pendingSpans.add('span2'); - privateClient._spanCompletionPromise = new Promise(() => {}); - privateClient._resolveSpanCompletion = () => {}; - - expect(privateClient._pendingSpans.size).toBe(2); - - client.dispose(); - - expect(privateClient._pendingSpans.size).toBe(0); - expect(privateClient._spanCompletionPromise).toBeNull(); - expect(privateClient._resolveSpanCompletion).toBeNull(); - }); - - it('clears flushLock reference', () => { - const mockContext = { - waitUntil: vi.fn(), - passThroughOnException: vi.fn(), - }; - const flushLock = makeFlushLock(mockContext as any); - - const client = new CloudflareClient({ - ...MOCK_CLIENT_OPTIONS, - flushLock, - }); - - const privateClient = client as unknown as { - _flushLock: ReturnType | void; - }; - - expect(privateClient._flushLock).toBeDefined(); - - client.dispose(); - - expect(privateClient._flushLock).toBeUndefined(); - }); - it('clears hooks', () => { const client = new CloudflareClient(MOCK_CLIENT_OPTIONS); - // Add a hook const hookCallback = vi.fn(); client.on('beforeEnvelope', hookCallback); @@ -99,13 +32,11 @@ describe('CloudflareClient', () => { _hooks: Record | undefined>; }; - // Verify hook was registered - check that there are hooks with actual Sets const hooksWithSets = Object.values(privateClient._hooks).filter(v => v instanceof Set); expect(hooksWithSets.length).toBeGreaterThan(0); client.dispose(); - // All hooks should be cleared (set to undefined) const hooksWithSetsAfter = Object.values(privateClient._hooks).filter(v => v instanceof Set); expect(hooksWithSetsAfter.length).toBe(0); }); @@ -113,14 +44,12 @@ describe('CloudflareClient', () => { it('clears event processors', () => { const client = new CloudflareClient(MOCK_CLIENT_OPTIONS); - // Add an event processor client.addEventProcessor(event => event); const privateClient = client as unknown as { _eventProcessors: unknown[]; }; - // SDK adds some default processors, so length should be >= 1 const initialLength = privateClient._eventProcessors.length; expect(initialLength).toBeGreaterThan(0); @@ -140,20 +69,17 @@ describe('CloudflareClient', () => { integrations: [mockIntegration], }); - // Need to call init() to setup integrations client.init(); const privateClient = client as unknown as { _integrations: Record; }; - // Integration should be registered expect(privateClient._integrations['MockIntegration']).toBeDefined(); expect(privateClient._integrations['MockIntegration']).not.toBeUndefined(); client.dispose(); - // Integration reference should be cleared (set to undefined) expect(privateClient._integrations['MockIntegration']).toBeUndefined(); }); @@ -178,17 +104,14 @@ describe('CloudflareClient', () => { _outcomes: Record; }; - // Add some outcomes privateClient._outcomes['reason:error:outcome1'] = 5; privateClient._outcomes['reason:error:outcome2'] = 10; - // Verify we have actual values const validOutcomes = Object.values(privateClient._outcomes).filter(v => v !== undefined); expect(validOutcomes.length).toBe(2); client.dispose(); - // All outcomes should be set to undefined const validOutcomesAfter = Object.values(privateClient._outcomes).filter(v => v !== undefined); expect(validOutcomesAfter.length).toBe(0); }); @@ -196,117 +119,11 @@ describe('CloudflareClient', () => { it('can be called multiple times safely', () => { const client = new CloudflareClient(MOCK_CLIENT_OPTIONS); - // Should not throw when called multiple times expect(() => { client.dispose(); client.dispose(); client.dispose(); }).not.toThrow(); }); - - it('does not break event emission after spanStart unsubscribe', () => { - const client = new CloudflareClient(MOCK_CLIENT_OPTIONS); - - // Dispose which unsubscribes from span events - client.dispose(); - - // Should not throw when emitting span events after dispose - expect(() => { - client.emit('spanStart', {} as any); - client.emit('spanEnd', {} as any); - }).not.toThrow(); - }); - }); - - describe('span lifecycle tracking', () => { - it('tracks pending spans when spanStart is emitted', () => { - const client = new CloudflareClient(MOCK_CLIENT_OPTIONS); - - const privateClient = client as unknown as { - _pendingSpans: Set; - _spanCompletionPromise: Promise | null; - }; - - expect(privateClient._pendingSpans.size).toBe(0); - expect(privateClient._spanCompletionPromise).toBeNull(); - - // Emit spanStart - const mockSpan = { - spanContext: () => ({ spanId: 'test-span-id' }), - }; - client.emit('spanStart', mockSpan as any); - - expect(privateClient._pendingSpans.has('test-span-id')).toBe(true); - expect(privateClient._spanCompletionPromise).not.toBeNull(); - }); - - it('removes pending span when spanEnd is emitted', async () => { - const client = new CloudflareClient(MOCK_CLIENT_OPTIONS); - - const privateClient = client as unknown as { - _pendingSpans: Set; - _spanCompletionPromise: Promise | null; - }; - - const mockSpan = { - spanContext: () => ({ spanId: 'test-span-id' }), - }; - - // Start span - client.emit('spanStart', mockSpan as any); - expect(privateClient._pendingSpans.has('test-span-id')).toBe(true); - - // End span - client.emit('spanEnd', mockSpan as any); - expect(privateClient._pendingSpans.has('test-span-id')).toBe(false); - }); - - it('resolves completion promise when all spans end', async () => { - const client = new CloudflareClient(MOCK_CLIENT_OPTIONS); - - const privateClient = client as unknown as { - _pendingSpans: Set; - _spanCompletionPromise: Promise | null; - }; - - const mockSpan1 = { spanContext: () => ({ spanId: 'span-1' }) }; - const mockSpan2 = { spanContext: () => ({ spanId: 'span-2' }) }; - - // Start both spans - client.emit('spanStart', mockSpan1 as any); - client.emit('spanStart', mockSpan2 as any); - - const completionPromise = privateClient._spanCompletionPromise; - expect(completionPromise).not.toBeNull(); - - // End first span - promise should still exist - client.emit('spanEnd', mockSpan1 as any); - expect(privateClient._pendingSpans.size).toBe(1); - - // End second span - promise should be resolved and reset - client.emit('spanEnd', mockSpan2 as any); - expect(privateClient._pendingSpans.size).toBe(0); - - // The original promise should resolve - await expect(completionPromise).resolves.toBeUndefined(); - }); - - it('does not track spans after dispose', () => { - const client = new CloudflareClient(MOCK_CLIENT_OPTIONS); - - client.dispose(); - - const privateClient = client as unknown as { - _pendingSpans: Set; - }; - - const mockSpan = { - spanContext: () => ({ spanId: 'test-span-id' }), - }; - - // Emit spanStart after dispose - should not be tracked - client.emit('spanStart', mockSpan as any); - expect(privateClient._pendingSpans.has('test-span-id')).toBe(false); - }); }); }); diff --git a/packages/cloudflare/test/durableobject.test.ts b/packages/cloudflare/test/durableobject.test.ts index efce592a6cdd..5f907750a5dc 100644 --- a/packages/cloudflare/test/durableobject.test.ts +++ b/packages/cloudflare/test/durableobject.test.ts @@ -1,10 +1,15 @@ import type { ExecutionContext } from '@cloudflare/workers-types'; import * as SentryCore from '@sentry/core'; -import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest'; import { instrumentDurableObjectWithSentry } from '../src'; import { getInstrumented } from '../src/instrument'; +import { resetSdk } from './testUtils'; describe('instrumentDurableObjectWithSentry', () => { + beforeEach(() => { + resetSdk(); + }); + afterEach(() => { vi.restoreAllMocks(); }); @@ -45,40 +50,37 @@ describe('instrumentDurableObjectWithSentry', () => { expect(await result).toBe('async-result'); }); - it('Instruments prototype methods without "sticking" to the options', () => { + it('Reuses the same client across multiple instances', () => { const mockContext = { waitUntil: vi.fn(), } as any; - const mockEnv = {} as any; // Environment mock - const initCore = vi.spyOn(SentryCore, 'initAndBind'); - vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); - const options = vi - .fn() - .mockReturnValueOnce({ - orgId: 1, - instrumentPrototypeMethods: true, - }) - .mockReturnValueOnce({ - orgId: 2, - instrumentPrototypeMethods: true, - }); + const mockEnv = {} as any; + let clientFromInstance1: SentryCore.Client | undefined; + let clientFromInstance2: SentryCore.Client | undefined; + const options = vi.fn().mockReturnValue({ + instrumentPrototypeMethods: true, + }); const testClass = class { - method() {} + method() { + return SentryCore.getClient(); + } }; const instance1 = Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), [ mockContext, mockEnv, ]); - instance1.method(); + clientFromInstance1 = instance1.method(); const instance2 = Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), [ mockContext, mockEnv, ]); - instance2.method(); + clientFromInstance2 = instance2.method(); - expect(initCore).nthCalledWith(1, expect.any(Function), expect.objectContaining({ orgId: 1 })); - expect(initCore).nthCalledWith(2, expect.any(Function), expect.objectContaining({ orgId: 2 })); + // Client is reused across instances + expect(clientFromInstance1).toBeDefined(); + expect(clientFromInstance2).toBeDefined(); + expect(clientFromInstance1).toBe(clientFromInstance2); }); it('All available durable object methods are instrumented when instrumentPrototypeMethods is enabled', () => { diff --git a/packages/cloudflare/test/flush.test.ts b/packages/cloudflare/test/flush.test.ts index 2a2b68aab02d..08c12707f865 100644 --- a/packages/cloudflare/test/flush.test.ts +++ b/packages/cloudflare/test/flush.test.ts @@ -1,38 +1,10 @@ -import { type ExecutionContext } from '@cloudflare/workers-types'; import * as sentryCore from '@sentry/core'; import { type Client } from '@sentry/core'; -import { describe, expect, it, onTestFinished, vi } from 'vitest'; -import { flushAndDispose, makeFlushLock } from '../src/flush'; - -describe('Flush buffer test', () => { - const waitUntilPromises: Promise[] = []; - const mockExecutionContext: ExecutionContext = { - waitUntil: vi.fn(prmise => { - waitUntilPromises.push(prmise); - }), - passThroughOnException: vi.fn(), - }; - it('should flush buffer immediately if no waitUntil were called', async () => { - const { finalize } = makeFlushLock(mockExecutionContext); - await expect(finalize()).resolves.toBeUndefined(); - }); - it('should flush buffer only after all waitUntil were finished', async () => { - vi.useFakeTimers(); - onTestFinished(() => { - vi.useRealTimers(); - }); - const task = new Promise(resolve => setTimeout(resolve, 100)); - const lock = makeFlushLock(mockExecutionContext); - mockExecutionContext.waitUntil(task); - void lock.finalize(); - vi.advanceTimersToNextTimer(); - await Promise.all(waitUntilPromises); - await expect(lock.ready).resolves.toBeUndefined(); - }); -}); +import { describe, expect, it, vi } from 'vitest'; +import { flushAndDispose } from '../src/flush'; describe('flushAndDispose', () => { - it('should flush and dispose the client when provided', async () => { + it('should flush the client when provided (without disposing for client reuse)', async () => { const mockClient = { flush: vi.fn().mockResolvedValue(true), dispose: vi.fn(), @@ -41,7 +13,8 @@ describe('flushAndDispose', () => { await flushAndDispose(mockClient, 3000); expect(mockClient.flush).toHaveBeenCalledWith(3000); - expect(mockClient.dispose).toHaveBeenCalled(); + // Note: dispose is no longer called since clients are reused across requests + expect(mockClient.dispose).not.toHaveBeenCalled(); }); it('should fall back to global flush when no client is provided', async () => { diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentEmail.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentEmail.test.ts index 5deb79d7d1c1..e76402e868c3 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentEmail.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentEmail.test.ts @@ -4,19 +4,16 @@ import type { ExecutionContext, ForwardableEmailMessage } from '@cloudflare/workers-types'; import type { Event } from '@sentry/core'; import * as SentryCore from '@sentry/core'; -import { beforeEach, describe, expect, onTestFinished, test, vi } from 'vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { CloudflareClient } from '../../../src/client'; import { withSentry } from '../../../src/withSentry'; +import { resetSdk } from '../../testUtils'; const MOCK_ENV = { SENTRY_DSN: 'https://public@dsn.ingest.sentry.io/1337', SENTRY_RELEASE: '1.1.1', }; -const MOCK_ENV_WITHOUT_DSN = { - SENTRY_RELEASE: '1.1.1', -}; - function createMockExecutionContext(): ExecutionContext { return { waitUntil: vi.fn(), @@ -37,13 +34,10 @@ function createMockEmailMessage(): ForwardableEmailMessage { }; } -function addDelayedWaitUntil(context: ExecutionContext) { - context.waitUntil(new Promise(resolve => setTimeout(() => resolve()))); -} - describe('instrumentEmail', () => { beforeEach(() => { vi.clearAllMocks(); + resetSdk(); }); test('does not double-wrap when withSentry is called twice', async () => { @@ -145,9 +139,10 @@ describe('instrumentEmail', () => { }); test('creates a cloudflare client and sets it on the handler', async () => { - const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind'); + let clientInsideHandler: SentryCore.Client | undefined; const handler = { email(_message, _env, _context) { + clientInsideHandler = SentryCore.getClient(); return; }, } satisfies ExportedHandler; @@ -155,8 +150,7 @@ describe('instrumentEmail', () => { const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV, createMockExecutionContext()); - expect(initAndBindSpy).toHaveBeenCalledTimes(1); - expect(initAndBindSpy).toHaveBeenLastCalledWith(CloudflareClient, expect.any(Object)); + expect(clientInsideHandler).toBeInstanceOf(CloudflareClient); }); describe('scope instrumentation', () => { @@ -273,30 +267,4 @@ describe('instrumentEmail', () => { }); }); }); - - test('flush must be called when all waitUntil are done', async () => { - const flush = vi.spyOn(SentryCore.Client.prototype, 'flush'); - vi.useFakeTimers(); - onTestFinished(() => { - vi.useRealTimers(); - }); - const handler = { - email(_controller, _env, _context) { - addDelayedWaitUntil(_context); - return; - }, - } satisfies ExportedHandler; - - const wrappedHandler = withSentry(vi.fn(), handler); - const waits: Promise[] = []; - const waitUntil = vi.fn(promise => waits.push(promise)); - await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV_WITHOUT_DSN, { - waitUntil, - } as unknown as ExecutionContext); - expect(flush).not.toBeCalled(); - expect(waitUntil).toBeCalled(); - vi.advanceTimersToNextTimer().runAllTimers(); - await Promise.all(waits); - expect(flush).toHaveBeenCalledOnce(); - }); }); diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentFetch.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentFetch.test.ts index 5486addb405c..ad11ea7a3803 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentFetch.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentFetch.test.ts @@ -3,19 +3,15 @@ import type { ExecutionContext } from '@cloudflare/workers-types'; import type { Event } from '@sentry/core'; -import * as SentryCore from '@sentry/core'; -import { beforeEach, describe, expect, onTestFinished, test, vi } from 'vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { withSentry } from '../../../src/withSentry'; +import { resetSdk } from '../../testUtils'; const MOCK_ENV = { SENTRY_DSN: 'https://public@dsn.ingest.sentry.io/1337', SENTRY_RELEASE: '1.1.1', }; -const MOCK_ENV_WITHOUT_DSN = { - SENTRY_RELEASE: '1.1.1', -}; - function createMockExecutionContext(): ExecutionContext { return { waitUntil: vi.fn(), @@ -23,13 +19,10 @@ function createMockExecutionContext(): ExecutionContext { }; } -function addDelayedWaitUntil(context: ExecutionContext) { - context.waitUntil(new Promise(resolve => setTimeout(() => resolve()))); -} - describe('instrumentFetch', () => { beforeEach(() => { vi.clearAllMocks(); + resetSdk(); }); test('does not double-wrap when withSentry is called twice', async () => { @@ -146,30 +139,4 @@ describe('instrumentFetch', () => { expect(sentryEvent.release).toEqual('2.0.0'); }); - - test('flush must be called when all waitUntil are done', async () => { - const flush = vi.spyOn(SentryCore.Client.prototype, 'flush'); - vi.useFakeTimers(); - onTestFinished(() => { - vi.useRealTimers(); - }); - const handler = { - fetch(_request, _env, _context) { - addDelayedWaitUntil(_context); - return new Response('test'); - }, - } satisfies ExportedHandler; - - const wrappedHandler = withSentry(vi.fn(), handler); - const waits: Promise[] = []; - const waitUntil = vi.fn(promise => waits.push(promise)); - await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV_WITHOUT_DSN, { - waitUntil, - } as unknown as ExecutionContext); - expect(flush).not.toBeCalled(); - expect(waitUntil).toBeCalled(); - vi.advanceTimersToNextTimer().runAllTimers(); - await Promise.all(waits); - expect(flush).toHaveBeenCalledOnce(); - }); }); diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentQueue.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentQueue.test.ts index 6930ff7180df..9b269db8b10f 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentQueue.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentQueue.test.ts @@ -4,19 +4,16 @@ import type { ExecutionContext, MessageBatch } from '@cloudflare/workers-types'; import type { Event } from '@sentry/core'; import * as SentryCore from '@sentry/core'; -import { beforeEach, describe, expect, onTestFinished, test, vi } from 'vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { CloudflareClient } from '../../../src/client'; import { withSentry } from '../../../src/withSentry'; +import { resetSdk } from '../../testUtils'; const MOCK_ENV = { SENTRY_DSN: 'https://public@dsn.ingest.sentry.io/1337', SENTRY_RELEASE: '1.1.1', }; -const MOCK_ENV_WITHOUT_DSN = { - SENTRY_RELEASE: '1.1.1', -}; - function createMockExecutionContext(): ExecutionContext { return { waitUntil: vi.fn(), @@ -50,13 +47,10 @@ function createMockQueueBatch(): MessageBatch { }; } -function addDelayedWaitUntil(context: ExecutionContext) { - context.waitUntil(new Promise(resolve => setTimeout(() => resolve()))); -} - describe('instrumentQueue', () => { beforeEach(() => { vi.clearAllMocks(); + resetSdk(); }); test('does not double-wrap when withSentry is called twice', async () => { @@ -158,9 +152,10 @@ describe('instrumentQueue', () => { }); test('creates a cloudflare client and sets it on the handler', async () => { - const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind'); + let clientInsideHandler: SentryCore.Client | undefined; const handler = { queue(_batch, _env, _context) { + clientInsideHandler = SentryCore.getClient(); return; }, } satisfies ExportedHandler; @@ -168,8 +163,7 @@ describe('instrumentQueue', () => { const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV, createMockExecutionContext()); - expect(initAndBindSpy).toHaveBeenCalledTimes(1); - expect(initAndBindSpy).toHaveBeenLastCalledWith(CloudflareClient, expect.any(Object)); + expect(clientInsideHandler).toBeInstanceOf(CloudflareClient); }); describe('scope instrumentation', () => { @@ -290,30 +284,4 @@ describe('instrumentQueue', () => { }); }); }); - - test('flush must be called when all waitUntil are done', async () => { - const flush = vi.spyOn(SentryCore.Client.prototype, 'flush'); - vi.useFakeTimers(); - onTestFinished(() => { - vi.useRealTimers(); - }); - const handler = { - queue(_controller, _env, _context) { - addDelayedWaitUntil(_context); - return; - }, - } satisfies ExportedHandler; - - const wrappedHandler = withSentry(vi.fn(), handler); - const waits: Promise[] = []; - const waitUntil = vi.fn(promise => waits.push(promise)); - await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV_WITHOUT_DSN, { - waitUntil, - } as unknown as ExecutionContext); - expect(flush).not.toBeCalled(); - expect(waitUntil).toBeCalled(); - vi.advanceTimersToNextTimer().runAllTimers(); - await Promise.all(waits); - expect(flush).toHaveBeenCalledOnce(); - }); }); diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentScheduled.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentScheduled.test.ts index 6b0107a68aef..a2ef810605f4 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentScheduled.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentScheduled.test.ts @@ -4,19 +4,16 @@ import type { ExecutionContext, ScheduledController } from '@cloudflare/workers-types'; import type { Event } from '@sentry/core'; import * as SentryCore from '@sentry/core'; -import { beforeEach, describe, expect, onTestFinished, test, vi } from 'vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { CloudflareClient } from '../../../src/client'; import { withSentry } from '../../../src/withSentry'; +import { resetSdk } from '../../testUtils'; const MOCK_ENV = { SENTRY_DSN: 'https://public@dsn.ingest.sentry.io/1337', SENTRY_RELEASE: '1.1.1', }; -const MOCK_ENV_WITHOUT_DSN = { - SENTRY_RELEASE: '1.1.1', -}; - function createMockExecutionContext(): ExecutionContext { return { waitUntil: vi.fn(), @@ -32,13 +29,10 @@ function createMockScheduledController(): ScheduledController { }; } -function addDelayedWaitUntil(context: ExecutionContext) { - context.waitUntil(new Promise(resolve => setTimeout(() => resolve()))); -} - describe('instrumentScheduled', () => { beforeEach(() => { vi.clearAllMocks(); + resetSdk(); }); test('does not double-wrap when withSentry is called twice', async () => { @@ -140,9 +134,10 @@ describe('instrumentScheduled', () => { }); test('creates a cloudflare client and sets it on the handler', async () => { - const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind'); + let clientInsideHandler: SentryCore.Client | undefined; const handler = { scheduled(_controller, _env, _context) { + clientInsideHandler = SentryCore.getClient(); return; }, } satisfies ExportedHandler; @@ -150,8 +145,7 @@ describe('instrumentScheduled', () => { const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); - expect(initAndBindSpy).toHaveBeenCalledTimes(1); - expect(initAndBindSpy).toHaveBeenLastCalledWith(CloudflareClient, expect.any(Object)); + expect(clientInsideHandler).toBeInstanceOf(CloudflareClient); }); describe('scope instrumentation', () => { @@ -269,30 +263,4 @@ describe('instrumentScheduled', () => { }); }); }); - - test('flush must be called when all waitUntil are done', async () => { - const flush = vi.spyOn(SentryCore.Client.prototype, 'flush'); - vi.useFakeTimers(); - onTestFinished(() => { - vi.useRealTimers(); - }); - const handler = { - scheduled(_controller, _env, _context) { - addDelayedWaitUntil(_context); - return; - }, - } satisfies ExportedHandler; - - const wrappedHandler = withSentry(vi.fn(), handler); - const waits: Promise[] = []; - const waitUntil = vi.fn(promise => waits.push(promise)); - await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV_WITHOUT_DSN, { - waitUntil, - } as unknown as ExecutionContext); - expect(flush).not.toBeCalled(); - expect(waitUntil).toBeCalled(); - vi.advanceTimersToNextTimer().runAllTimers(); - await Promise.all(waits); - expect(flush).toHaveBeenCalledOnce(); - }); }); diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentTail.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentTail.test.ts index 4f47dc3c62c7..eba2e1846d37 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentTail.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentTail.test.ts @@ -4,19 +4,16 @@ import type { ExecutionContext, TraceItem } from '@cloudflare/workers-types'; import type { Event } from '@sentry/core'; import * as SentryCore from '@sentry/core'; -import { beforeEach, describe, expect, onTestFinished, test, vi } from 'vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { CloudflareClient } from '../../../src/client'; import { withSentry } from '../../../src/withSentry'; +import { resetSdk } from '../../testUtils'; const MOCK_ENV = { SENTRY_DSN: 'https://public@dsn.ingest.sentry.io/1337', SENTRY_RELEASE: '1.1.1', }; -const MOCK_ENV_WITHOUT_DSN = { - SENTRY_RELEASE: '1.1.1', -}; - function createMockExecutionContext(): ExecutionContext { return { waitUntil: vi.fn(), @@ -51,13 +48,10 @@ function createMockTailEvent(): TraceItem[] { ]; } -function addDelayedWaitUntil(context: ExecutionContext) { - context.waitUntil(new Promise(resolve => setTimeout(() => resolve()))); -} - describe('instrumentTail', () => { beforeEach(() => { vi.clearAllMocks(); + resetSdk(); }); test('does not double-wrap when withSentry is called twice', async () => { @@ -159,9 +153,10 @@ describe('instrumentTail', () => { }); test('creates a cloudflare client and sets it on the handler', async () => { - const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind'); + let clientInsideHandler: SentryCore.Client | undefined; const handler = { tail(_event, _env, _context) { + clientInsideHandler = SentryCore.getClient(); return; }, } satisfies ExportedHandler; @@ -169,8 +164,7 @@ describe('instrumentTail', () => { const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV, createMockExecutionContext()); - expect(initAndBindSpy).toHaveBeenCalledTimes(1); - expect(initAndBindSpy).toHaveBeenLastCalledWith(CloudflareClient, expect.any(Object)); + expect(clientInsideHandler).toBeInstanceOf(CloudflareClient); }); describe('scope instrumentation', () => { @@ -245,31 +239,4 @@ describe('instrumentTail', () => { expect(thrownError).toBe(error); }); }); - - test('flush must be called when all waitUntil are done', async () => { - const flush = vi.spyOn(SentryCore.Client.prototype, 'flush'); - vi.useFakeTimers(); - onTestFinished(() => { - vi.useRealTimers(); - flush.mockRestore(); - }); - const handler = { - tail(_controller, _env, _context) { - addDelayedWaitUntil(_context); - return; - }, - } satisfies ExportedHandler; - - const wrappedHandler = withSentry(vi.fn(), handler); - const waits: Promise[] = []; - const waitUntil = vi.fn(promise => waits.push(promise)); - await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV_WITHOUT_DSN, { - waitUntil, - } as unknown as ExecutionContext); - expect(flush).not.toBeCalled(); - expect(waitUntil).toBeCalled(); - vi.advanceTimersToNextTimer().runAllTimers(); - await Promise.all(waits); - expect(flush).toHaveBeenCalledOnce(); - }); }); diff --git a/packages/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts index 5160d8976e9b..146c86984326 100644 --- a/packages/cloudflare/test/request.test.ts +++ b/packages/cloudflare/test/request.test.ts @@ -9,6 +9,7 @@ import { setAsyncLocalStorageAsyncContextStrategy } from '../src/async'; import type { CloudflareOptions } from '../src/client'; import { CloudflareClient } from '../src/client'; import { wrapRequestHandler } from '../src/request'; +import { resetSdk } from './testUtils'; const MOCK_OPTIONS: CloudflareOptions = { dsn: 'https://public@dsn.ingest.sentry.io/1337', @@ -25,6 +26,7 @@ describe('withSentry', () => { beforeEach(() => { vi.clearAllMocks(); + resetSdk(); }); test('passes through the response from the handler', async () => { @@ -79,14 +81,16 @@ describe('withSentry', () => { }); test('creates a cloudflare client and sets it on the handler', async () => { - const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind'); + let clientInsideHandler: SentryCore.Client | undefined; await wrapRequestHandler( { options: MOCK_OPTIONS, request: new Request('https://example.com'), context: createMockExecutionContext() }, - () => new Response('test'), + () => { + clientInsideHandler = SentryCore.getClient(); + return new Response('test'); + }, ); - expect(initAndBindSpy).toHaveBeenCalledTimes(1); - expect(initAndBindSpy).toHaveBeenLastCalledWith(CloudflareClient, expect.any(Object)); + expect(clientInsideHandler).toBeInstanceOf(CloudflareClient); }); test('flush must be called when all waitUntil are done', async () => { @@ -378,8 +382,11 @@ function createMockExecutionContext(): ExecutionContext { }; } -describe('flushAndDispose', () => { - test('dispose is called after flush completes', async () => { +describe('client reuse (dispose is not called)', () => { + // Note: dispose is no longer called because clients are reused across requests. + // These tests verify that flush is called without dispose. + + test('flush is called but dispose is not (client reuse)', async () => { const context = createMockExecutionContext(); const waits: Promise[] = []; const waitUntil = vi.fn(promise => waits.push(promise)); @@ -398,13 +405,13 @@ describe('flushAndDispose', () => { await Promise.all(waits); expect(flushSpy).toHaveBeenCalled(); - expect(disposeSpy).toHaveBeenCalled(); + expect(disposeSpy).not.toHaveBeenCalled(); flushSpy.mockRestore(); disposeSpy.mockRestore(); }); - test('dispose is called after handler throws error', async () => { + test('flush is called after handler throws error (without dispose)', async () => { const context = createMockExecutionContext(); const waits: Promise[] = []; const waitUntil = vi.fn(promise => waits.push(promise)); @@ -424,13 +431,14 @@ describe('flushAndDispose', () => { // Wait for all waitUntil promises to resolve await Promise.all(waits); - expect(disposeSpy).toHaveBeenCalled(); + expect(flushSpy).toHaveBeenCalled(); + expect(disposeSpy).not.toHaveBeenCalled(); flushSpy.mockRestore(); disposeSpy.mockRestore(); }); - test('dispose is called for OPTIONS requests', async () => { + test('flush is called for OPTIONS requests (without dispose)', async () => { const context = createMockExecutionContext(); const waits: Promise[] = []; const waitUntil = vi.fn(promise => waits.push(promise)); @@ -451,13 +459,14 @@ describe('flushAndDispose', () => { // Wait for all waitUntil promises to resolve await Promise.all(waits); - expect(disposeSpy).toHaveBeenCalled(); + expect(flushSpy).toHaveBeenCalled(); + expect(disposeSpy).not.toHaveBeenCalled(); flushSpy.mockRestore(); disposeSpy.mockRestore(); }); - test('dispose is called for HEAD requests', async () => { + test('flush is called for HEAD requests (without dispose)', async () => { const context = createMockExecutionContext(); const waits: Promise[] = []; const waitUntil = vi.fn(promise => waits.push(promise)); @@ -478,13 +487,14 @@ describe('flushAndDispose', () => { // Wait for all waitUntil promises to resolve await Promise.all(waits); - expect(disposeSpy).toHaveBeenCalled(); + expect(flushSpy).toHaveBeenCalled(); + expect(disposeSpy).not.toHaveBeenCalled(); flushSpy.mockRestore(); disposeSpy.mockRestore(); }); - test('dispose is called after streaming response completes', async () => { + test('flush is called after streaming response completes (without dispose)', async () => { const context = createMockExecutionContext(); const waits: Promise[] = []; const waitUntil = vi.fn(promise => waits.push(promise)); @@ -512,7 +522,8 @@ describe('flushAndDispose', () => { // Wait for all waitUntil promises to resolve await Promise.all(waits); - expect(disposeSpy).toHaveBeenCalled(); + expect(flushSpy).toHaveBeenCalled(); + expect(disposeSpy).not.toHaveBeenCalled(); flushSpy.mockRestore(); disposeSpy.mockRestore(); diff --git a/packages/cloudflare/test/sdk.test.ts b/packages/cloudflare/test/sdk.test.ts index 54b8ee609cda..1ec4f145f04a 100644 --- a/packages/cloudflare/test/sdk.test.ts +++ b/packages/cloudflare/test/sdk.test.ts @@ -22,6 +22,13 @@ describe('init', () => { expect(client).toBeInstanceOf(CloudflareClient); }); + test('should reuse existing client', () => { + const client1 = init({}); + const client2 = init({}); + + expect(client1).toBe(client2); + }); + test('installs SpanStreaming integration when traceLifecycle is "stream"', () => { init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', diff --git a/packages/cloudflare/test/testUtils.ts b/packages/cloudflare/test/testUtils.ts index 8dcd3d43a4d9..831a04ac79a0 100644 --- a/packages/cloudflare/test/testUtils.ts +++ b/packages/cloudflare/test/testUtils.ts @@ -5,7 +5,9 @@ function resetGlobals(): void { getCurrentScope().clear(); getCurrentScope().setClient(undefined); getIsolationScope().clear(); + getIsolationScope().setClient(undefined); getGlobalScope().clear(); + getGlobalScope().setClient(undefined); } function cleanupOtel(): void { diff --git a/packages/cloudflare/test/workflow.test.ts b/packages/cloudflare/test/workflow.test.ts index 14bb7e78a90e..470a72f2dd56 100644 --- a/packages/cloudflare/test/workflow.test.ts +++ b/packages/cloudflare/test/workflow.test.ts @@ -3,6 +3,7 @@ import { startSpan } from '@sentry/core'; import type { WorkflowEvent, WorkflowStep, WorkflowStepConfig } from 'cloudflare:workers'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { deterministicTraceIdFromInstanceId, instrumentWorkflowWithSentry } from '../src/workflows'; +import { resetSdk } from './testUtils'; vi.mock('../src/instrumentations/worker/instrumentEnv', () => ({ instrumentEnv: vi.fn((env: unknown) => env), @@ -77,6 +78,7 @@ const TRACE_ID = INSTANCE_ID.replace(/-/g, ''); describe.skipIf(NODE_MAJOR_VERSION < 20)('workflows', () => { beforeEach(() => { vi.clearAllMocks(); + resetSdk(); }); test('hashStringToUuid hashes a string to a UUID for Sentry trace ID', async () => { diff --git a/packages/cloudflare/test/wrapMethodWithSentry.test.ts b/packages/cloudflare/test/wrapMethodWithSentry.test.ts index a812258c2c94..8481dfd4f103 100644 --- a/packages/cloudflare/test/wrapMethodWithSentry.test.ts +++ b/packages/cloudflare/test/wrapMethodWithSentry.test.ts @@ -154,8 +154,9 @@ describe('wrapMethodWithSentry', () => { put: vi.fn().mockResolvedValue(undefined), }; const waitUntilPromises: Promise[] = []; + const originalWaitUntil = vi.fn((p: Promise) => waitUntilPromises.push(p)); const context = { - waitUntil: vi.fn((p: Promise) => waitUntilPromises.push(p)), + waitUntil: originalWaitUntil, originalStorage: mockStorage, } as any; @@ -174,7 +175,8 @@ describe('wrapMethodWithSentry', () => { expect(result).toBe('sync-result'); // The link fetching happens via waitUntil, not blocking the response - expect(context.waitUntil).toHaveBeenCalled(); + // Note: makeFlushLock wraps the waitUntil, but the original still gets called + expect(waitUntilPromises.length).toBeGreaterThan(0); }); it('marks handler as instrumented', () => { diff --git a/packages/core/src/integrations/dedupe.ts b/packages/core/src/integrations/dedupe.ts index 4379c8f6ff73..43f268268c16 100644 --- a/packages/core/src/integrations/dedupe.ts +++ b/packages/core/src/integrations/dedupe.ts @@ -1,3 +1,5 @@ +import type { Scope } from '../scope'; +import { getIsolationScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import { defineIntegration } from '../integration'; import type { Event } from '../types-hoist/event'; @@ -10,7 +12,10 @@ import { getFramesFromEvent } from '../utils/stacktrace'; const INTEGRATION_NAME = 'Dedupe'; const _dedupeIntegration = (() => { - let previousEvent: Event | undefined; + // Store previousEvent per isolation scope to avoid cross-request interference + // when clients are reused (e.g., in Cloudflare Workers). + // WeakMap ensures automatic cleanup when scopes are garbage collected. + const previousEventByScope = new WeakMap(); return { name: INTEGRATION_NAME, @@ -21,6 +26,9 @@ const _dedupeIntegration = (() => { return currentEvent; } + const isolationScope = getIsolationScope(); + const previousEvent = previousEventByScope.get(isolationScope); + // Juuust in case something goes wrong try { if (_shouldDropEvent(currentEvent, previousEvent)) { @@ -29,7 +37,8 @@ const _dedupeIntegration = (() => { } } catch {} // eslint-disable-line no-empty - return (previousEvent = currentEvent); + previousEventByScope.set(isolationScope, currentEvent); + return currentEvent; }, }; }) satisfies IntegrationFn; diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index 6ca5594b3da6..f7b5ab54c49e 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -9,7 +9,7 @@ const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope'; type ScopeWeakRef = { deref(): Scope | undefined } | Scope; type SpanWithScopes = Span & { - [SCOPE_ON_START_SPAN_FIELD]?: Scope; + [SCOPE_ON_START_SPAN_FIELD]?: ScopeWeakRef; [ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: ScopeWeakRef; }; @@ -51,9 +51,7 @@ function unwrapScopeFromWeakRef(scopeRef: ScopeWeakRef | undefined): Scope | und export function setCapturedScopesOnSpan(span: Span | undefined, scope: Scope, isolationScope: Scope): void { if (span) { addNonEnumerableProperty(span, ISOLATION_SCOPE_ON_START_SPAN_FIELD, wrapScopeWithWeakRef(isolationScope)); - // We don't wrap the scope with a WeakRef here because webkit aggressively garbage collects - // and scopes are not held in memory for long periods of time. - addNonEnumerableProperty(span, SCOPE_ON_START_SPAN_FIELD, scope); + addNonEnumerableProperty(span, SCOPE_ON_START_SPAN_FIELD, wrapScopeWithWeakRef(scope)); } } @@ -65,7 +63,7 @@ export function getCapturedScopesOnSpan(span: Span): { scope?: Scope; isolationS const spanWithScopes = span as SpanWithScopes; return { - scope: spanWithScopes[SCOPE_ON_START_SPAN_FIELD], + scope: unwrapScopeFromWeakRef(spanWithScopes[SCOPE_ON_START_SPAN_FIELD]), isolationScope: unwrapScopeFromWeakRef(spanWithScopes[ISOLATION_SCOPE_ON_START_SPAN_FIELD]), }; } diff --git a/packages/core/test/lib/tracing/utils.test.ts b/packages/core/test/lib/tracing/utils.test.ts index 5fe44f4199ea..067f805eb4aa 100644 --- a/packages/core/test/lib/tracing/utils.test.ts +++ b/packages/core/test/lib/tracing/utils.test.ts @@ -44,17 +44,16 @@ describe('tracing utils', () => { expect(retrieved.isolationScope).toBeUndefined(); }); - it('uses WeakRef only for isolation scopes', () => { + it('uses WeakRef for both scopes', () => { const span = createMockSpan(); const scope = new Scope(); const isolationScope = new Scope(); setCapturedScopesOnSpan(span, scope, isolationScope); - // Check that only isolation scope is wrapped with WeakRef const spanWithScopes = span as any; - expect(spanWithScopes._sentryScope).toBe(scope); // Regular scope stored directly - expect(spanWithScopes._sentryIsolationScope).toBeInstanceOf(WeakRef); // Isolation scope wrapped + expect(spanWithScopes._sentryScope).toBeInstanceOf(WeakRef); + expect(spanWithScopes._sentryIsolationScope).toBeInstanceOf(WeakRef); // Verify we can still retrieve the scopes const retrieved = getCapturedScopesOnSpan(span); diff --git a/yarn.lock b/yarn.lock index 7c65cb69ae99..a718a01b0bf2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,25 +2,6 @@ # yarn lockfile v1 -"@actions/artifact@5.0.3": - version "5.0.3" - resolved "https://registry.yarnpkg.com/@actions/artifact/-/artifact-5.0.3.tgz#e3ca31d98a5836c23d4c5b829429b5aa6f71f0ff" - integrity sha512-FIEG8Kum0wABZnktJvFi1xuVPc31xrunhZwLCvjrCGISQOm0ifyo7cjqf6PHiEeqoWMa5HIGOsB+lGM4aKCseA== - dependencies: - "@actions/core" "^2.0.0" - "@actions/github" "^6.0.1" - "@actions/http-client" "^3.0.2" - "@azure/storage-blob" "^12.29.1" - "@octokit/core" "^5.2.1" - "@octokit/plugin-request-log" "^1.0.4" - "@octokit/plugin-retry" "^3.0.9" - "@octokit/request" "^8.4.1" - "@octokit/request-error" "^5.1.1" - "@protobuf-ts/plugin" "^2.2.3-alpha.1" - archiver "^7.0.1" - jwt-decode "^3.1.2" - unzip-stream "^0.3.1" - "@actions/artifact@^6.1.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@actions/artifact/-/artifact-6.1.0.tgz#6d30eb1837b1f047dce2ebe364aa60a7881f202d" @@ -49,14 +30,6 @@ "@actions/http-client" "^2.0.1" uuid "^8.3.2" -"@actions/core@^2.0.0": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@actions/core/-/core-2.0.3.tgz#b05e8cf407ab393e5d10282357a74e1ee2315eee" - integrity sha512-Od9Thc3T1mQJYddvVPM4QGiLUewdh+3txmDYHHxoNdkqysR1MbCT+rFOtNUxYAz+7+6RIsqipVahY2GJqGPyxA== - dependencies: - "@actions/exec" "^2.0.0" - "@actions/http-client" "^3.0.2" - "@actions/core@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@actions/core/-/core-3.0.0.tgz#89cb07c119e9b46a649ad5f355e77de9b3108cf8" @@ -72,13 +45,6 @@ dependencies: "@actions/io" "^1.0.1" -"@actions/exec@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@actions/exec/-/exec-2.0.0.tgz#35e829723389f80e362ec2cc415697ec74362ad8" - integrity sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw== - dependencies: - "@actions/io" "^2.0.0" - "@actions/exec@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@actions/exec/-/exec-3.0.0.tgz#8c3464d20f0aa4068707757021d7e3c01a7ee203" @@ -96,19 +62,6 @@ "@octokit/plugin-paginate-rest" "^2.17.0" "@octokit/plugin-rest-endpoint-methods" "^5.13.0" -"@actions/github@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@actions/github/-/github-6.0.1.tgz#76e5f96df062c90635a7181ef45ff1c4ac21306e" - integrity sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw== - dependencies: - "@actions/http-client" "^2.2.0" - "@octokit/core" "^5.0.1" - "@octokit/plugin-paginate-rest" "^9.2.2" - "@octokit/plugin-rest-endpoint-methods" "^10.4.0" - "@octokit/request" "^8.4.1" - "@octokit/request-error" "^5.1.1" - undici "^5.28.5" - "@actions/github@^9.0.0": version "9.0.0" resolved "https://registry.yarnpkg.com/@actions/github/-/github-9.0.0.tgz#c86dae4128b2a6987271e2663bee9e766464840a" @@ -130,7 +83,7 @@ "@actions/core" "^3.0.0" minimatch "^3.0.4" -"@actions/http-client@^2.0.1", "@actions/http-client@^2.2.0": +"@actions/http-client@^2.0.1": version "2.2.3" resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.2.3.tgz#31fc0b25c0e665754ed39a9f19a8611fc6dab674" integrity sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA== @@ -159,11 +112,6 @@ resolved "https://registry.yarnpkg.com/@actions/io/-/io-1.1.3.tgz#4cdb6254da7962b07473ff5c335f3da485d94d71" integrity sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q== -"@actions/io@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@actions/io/-/io-2.0.0.tgz#3ad1271ba3cd515324f2215e8d4c1c0c3864d65b" - integrity sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg== - "@actions/io@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@actions/io/-/io-3.0.2.tgz#6f89b27a159d109836d983efa283997c23b92284" @@ -578,11 +526,6 @@ resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" integrity sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg== -"@assemblyscript/loader@^0.19.21": - version "0.19.23" - resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.19.23.tgz#7fccae28d0a2692869f1d1219d36093bc24d5e72" - integrity sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw== - "@astrojs/compiler@^2.3.0": version "2.12.2" resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-2.12.2.tgz#5913b6ec7efffebdfb37fae9a50122802ae08c64" @@ -1581,7 +1524,7 @@ jsonwebtoken "^9.0.0" uuid "^8.3.0" -"@azure/storage-blob@^12.29.1", "@azure/storage-blob@^12.30.0": +"@azure/storage-blob@^12.30.0": version "12.31.0" resolved "https://registry.yarnpkg.com/@azure/storage-blob/-/storage-blob-12.31.0.tgz#97b09be2bf6ab59739b862edd8124798362ce720" integrity sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg== @@ -3097,11 +3040,6 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== - "@colors/colors@1.6.0", "@colors/colors@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" @@ -5340,13 +5278,6 @@ semver "^7.5.3" tar "^7.4.0" -"@minimistjs/subarg@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@minimistjs/subarg/-/subarg-1.0.0.tgz#484fdfebda9dc32087d7c7999ec6350684fb42d2" - integrity sha512-Q/ONBiM2zNeYUy0mVSO44mWWKYM3UHuEK43PKIOzJCbvUnPoMH1K+gk3cf1kgnCVJFlWmddahQQCmrmBGlk9jQ== - dependencies: - minimist "^1.1.0" - "@mjackson/node-fetch-server@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz#577c0c25d8aae9f69a97738b7b0d03d1471cdc49" @@ -5919,11 +5850,6 @@ dependencies: "@octokit/types" "^6.0.3" -"@octokit/auth-token@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-4.0.0.tgz#40d203ea827b9f17f42a29c6afb93b7745ef80c7" - integrity sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA== - "@octokit/auth-token@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-6.0.0.tgz#b02e9c08a2d8937df09a2a981f226ad219174c53" @@ -5942,19 +5868,6 @@ before-after-hook "^2.2.0" universal-user-agent "^6.0.0" -"@octokit/core@^5.0.1", "@octokit/core@^5.2.1": - version "5.2.2" - resolved "https://registry.yarnpkg.com/@octokit/core/-/core-5.2.2.tgz#252805732de9b4e8e4f658d34b80c4c9b2534761" - integrity sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg== - dependencies: - "@octokit/auth-token" "^4.0.0" - "@octokit/graphql" "^7.1.0" - "@octokit/request" "^8.4.1" - "@octokit/request-error" "^5.1.1" - "@octokit/types" "^13.0.0" - before-after-hook "^2.2.0" - universal-user-agent "^6.0.0" - "@octokit/core@^7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-7.0.6.tgz#0d58704391c6b681dec1117240ea4d2a98ac3916" @@ -5985,14 +5898,6 @@ is-plain-object "^5.0.0" universal-user-agent "^6.0.0" -"@octokit/endpoint@^9.0.6": - version "9.0.6" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-9.0.6.tgz#114d912108fe692d8b139cfe7fc0846dfd11b6c0" - integrity sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw== - dependencies: - "@octokit/types" "^13.1.0" - universal-user-agent "^6.0.0" - "@octokit/graphql@^4.5.8": version "4.8.0" resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3" @@ -6002,15 +5907,6 @@ "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" -"@octokit/graphql@^7.1.0": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-7.1.1.tgz#79d9f3d0c96a8fd13d64186fe5c33606d48b79cc" - integrity sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g== - dependencies: - "@octokit/request" "^8.4.1" - "@octokit/types" "^13.0.0" - universal-user-agent "^6.0.0" - "@octokit/graphql@^9.0.3": version "9.0.3" resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-9.0.3.tgz#5b8341c225909e924b466705c13477face869456" @@ -6025,16 +5921,6 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-12.11.0.tgz#da5638d64f2b919bca89ce6602d059f1b52d3ef0" integrity sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ== -"@octokit/openapi-types@^20.0.0": - version "20.0.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-20.0.0.tgz#9ec2daa0090eeb865ee147636e0c00f73790c6e5" - integrity sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA== - -"@octokit/openapi-types@^24.2.0": - version "24.2.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-24.2.0.tgz#3d55c32eac0d38da1a7083a9c3b0cca77924f7d3" - integrity sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg== - "@octokit/openapi-types@^27.0.0": version "27.0.0" resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-27.0.0.tgz#374ea53781965fd02a9d36cacb97e152cefff12d" @@ -6054,30 +5940,11 @@ dependencies: "@octokit/types" "^6.40.0" -"@octokit/plugin-paginate-rest@^9.2.2": - version "9.2.2" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz#c516bc498736bcdaa9095b9a1d10d9d0501ae831" - integrity sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ== - dependencies: - "@octokit/types" "^12.6.0" - -"@octokit/plugin-request-log@^1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" - integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== - "@octokit/plugin-request-log@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz#de1c1e557df6c08adb631bf78264fa741e01b317" integrity sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q== -"@octokit/plugin-rest-endpoint-methods@^10.4.0": - version "10.4.1" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz#41ba478a558b9f554793075b2e20cd2ef973be17" - integrity sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg== - dependencies: - "@octokit/types" "^12.6.0" - "@octokit/plugin-rest-endpoint-methods@^17.0.0": version "17.0.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz#8c54397d3a4060356a1c8a974191ebf945924105" @@ -6093,14 +5960,6 @@ "@octokit/types" "^6.39.0" deprecation "^2.3.1" -"@octokit/plugin-retry@^3.0.9": - version "3.0.9" - resolved "https://registry.yarnpkg.com/@octokit/plugin-retry/-/plugin-retry-3.0.9.tgz#ae625cca1e42b0253049102acd71c1d5134788fe" - integrity sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ== - dependencies: - "@octokit/types" "^6.0.3" - bottleneck "^2.15.3" - "@octokit/plugin-retry@^8.0.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-retry/-/plugin-retry-8.1.0.tgz#e25c2fb5e0a09cfe674ef9df75d7ca4fafa16c11" @@ -6119,15 +5978,6 @@ deprecation "^2.0.0" once "^1.4.0" -"@octokit/request-error@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-5.1.1.tgz#b9218f9c1166e68bb4d0c89b638edc62c9334805" - integrity sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g== - dependencies: - "@octokit/types" "^13.1.0" - deprecation "^2.0.0" - once "^1.4.0" - "@octokit/request-error@^7.0.2", "@octokit/request-error@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-7.1.0.tgz#440fa3cae310466889778f5a222b47a580743638" @@ -6158,30 +6008,6 @@ node-fetch "^2.6.7" universal-user-agent "^6.0.0" -"@octokit/request@^8.4.1": - version "8.4.1" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-8.4.1.tgz#715a015ccf993087977ea4365c44791fc4572486" - integrity sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw== - dependencies: - "@octokit/endpoint" "^9.0.6" - "@octokit/request-error" "^5.1.1" - "@octokit/types" "^13.1.0" - universal-user-agent "^6.0.0" - -"@octokit/types@^12.6.0": - version "12.6.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-12.6.0.tgz#8100fb9eeedfe083aae66473bd97b15b62aedcb2" - integrity sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw== - dependencies: - "@octokit/openapi-types" "^20.0.0" - -"@octokit/types@^13.0.0", "@octokit/types@^13.1.0": - version "13.10.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.10.0.tgz#3e7c6b19c0236c270656e4ea666148c2b51fd1a3" - integrity sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA== - dependencies: - "@octokit/openapi-types" "^24.2.0" - "@octokit/types@^16.0.0": version "16.0.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-16.0.0.tgz#fbd7fa590c2ef22af881b1d79758bfaa234dbb7c" @@ -11789,35 +11615,6 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== -autocannon@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-8.0.0.tgz#72b3ade6ec63dca0dc3be157c873d0a27e3f3745" - integrity sha512-fMMcWc2JPFcUaqHeR6+PbmEpTxCrPZyBUM95oG4w3ngJ8NfBNas/ZXA+pTHXLqJ0UlFVTcy05GC25WxKx/M20A== - dependencies: - "@minimistjs/subarg" "^1.0.0" - chalk "^4.1.0" - char-spinner "^1.0.1" - cli-table3 "^0.6.0" - color-support "^1.1.1" - cross-argv "^2.0.0" - form-data "^4.0.0" - has-async-hooks "^1.0.0" - hdr-histogram-js "^3.0.0" - hdr-histogram-percentiles-obj "^3.0.0" - http-parser-js "^0.5.2" - hyperid "^3.0.0" - lodash.chunk "^4.2.0" - lodash.clonedeep "^4.5.0" - lodash.flatten "^4.4.0" - manage-path "^2.0.0" - on-net-listen "^1.1.1" - pretty-bytes "^5.4.1" - progress "^2.0.3" - reinterval "^1.1.0" - retimer "^3.0.0" - semver "^7.3.2" - timestring "^6.0.0" - autoprefixer@^10.4.13, autoprefixer@^10.4.19, autoprefixer@^10.4.21, autoprefixer@^10.4.8: version "10.4.24" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.24.tgz#2c29595f3abd820a79976a609d0bf40eecf212fb" @@ -12972,7 +12769,7 @@ buffer-more-ints@~1.0.0: resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz#ef4f8e2dddbad429ed3828a9c55d44f05c611422" integrity sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg== -buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: +buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -13323,11 +13120,6 @@ chalk@^5.0.0, chalk@^5.2.0, chalk@^5.3.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== -char-spinner@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/char-spinner/-/char-spinner-1.0.1.tgz#e6ea67bd247e107112983b7ab0479ed362800081" - integrity sha512-acv43vqJ0+N0rD+Uw3pDHSxP30FHrywu2NO6/wBaHChJIizpDeBUd6NjqhNhy9LGaEAhZAXn46QzmlAvIWd16g== - character-entities-html4@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" @@ -13518,15 +13310,6 @@ cli-spinners@^2.0.0, cli-spinners@^2.5.0, cli-spinners@^2.9.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.1.tgz#9c0b9dad69a6d47cbb4333c14319b060ed395a35" integrity sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ== -cli-table3@^0.6.0: - version "0.6.5" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" - integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== - dependencies: - string-width "^4.2.0" - optionalDependencies: - "@colors/colors" "1.5.0" - cli-table@^0.3.1: version "0.3.6" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.6.tgz#e9d6aa859c7fe636981fd3787378c2a20bce92fc" @@ -13656,7 +13439,7 @@ color-string@^1.6.0, color-string@^1.9.0: color-name "^1.0.0" simple-swizzle "^0.2.2" -color-support@^1.1.1, color-support@^1.1.3: +color-support@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== @@ -14150,11 +13933,6 @@ croner@^9.1.0: resolved "https://registry.yarnpkg.com/croner/-/croner-9.1.0.tgz#94ccbba2570bca329f60f36ec19875dccf9a63aa" integrity sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g== -cross-argv@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/cross-argv/-/cross-argv-2.0.0.tgz#2e7907ba3246f82c967623a3e8525925bbd6c0ad" - integrity sha512-YIaY9TR5Nxeb8SMdtrU8asWVM4jqJDNDYlKV21LxtYcfNJhp1kEsgSa6qXwXgzN0WQWGODps0+TlGp2xQSHwOg== - cross-inspect@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cross-inspect/-/cross-inspect-1.0.1.tgz#15f6f65e4ca963cf4cc1a2b5fef18f6ca328712b" @@ -18597,11 +18375,6 @@ has-ansi@^3.0.0: dependencies: ansi-regex "^3.0.0" -has-async-hooks@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-async-hooks/-/has-async-hooks-1.0.0.tgz#3df965ade8cd2d9dbfdacfbca3e0a5152baaf204" - integrity sha512-YF0VPGjkxr7AyyQQNykX8zK4PvtEDsUJAPqwu06UFz1lb6EvI53sPh5H1kWxg8NXI5LsfRCZ8uX9NkYDZBb/mw== - has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -18813,15 +18586,6 @@ hdr-histogram-js@^2.0.1: base64-js "^1.2.0" pako "^1.0.3" -hdr-histogram-js@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-3.0.1.tgz#b281e90d6ca80ee656bc378dafa39d7239b90855" - integrity sha512-l3GSdZL1Jr1C0kyb461tUjEdrRPZr8Qry7jByltf5JGrA0xvqOSrxRBfcrJqqV/AMEtqqhHhC6w8HW0gn76tRQ== - dependencies: - "@assemblyscript/loader" "^0.19.21" - base64-js "^1.2.0" - pako "^1.0.3" - hdr-histogram-percentiles-obj@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz#9409f4de0c2dda78e61de2d9d78b1e9f3cba283c" @@ -19073,7 +18837,7 @@ http-errors@~1.6.2: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" -http-parser-js@>=0.5.1, http-parser-js@^0.5.2: +http-parser-js@>=0.5.1: version "0.5.10" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075" integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA== @@ -19192,15 +18956,6 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -hyperid@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/hyperid/-/hyperid-3.3.0.tgz#2042bb296b7f1d5ba0797a5705469af0899c8556" - integrity sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ== - dependencies: - buffer "^5.2.1" - uuid "^8.3.2" - uuid-parse "^1.1.0" - iconv-lite@0.6.3, iconv-lite@^0.6.2, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -20575,11 +20330,6 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" -jwt-decode@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" - integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== - jwt-decode@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" @@ -21001,11 +20751,6 @@ lodash.camelcase@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= -lodash.chunk@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" - integrity sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w== - lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -21041,11 +20786,6 @@ lodash.flatten@^3.0.2: lodash._baseflatten "^3.0.0" lodash._isiterateecall "^3.0.0" -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== - lodash.foreach@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" @@ -21440,11 +21180,6 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" -manage-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/manage-path/-/manage-path-2.0.0.tgz#f4cf8457b926eeee2a83b173501414bc76eb9597" - integrity sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A== - map-age-cleaner@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" @@ -22264,7 +21999,7 @@ minimist@^0.2.1: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.4.tgz#0085d5501e29033748a2f2a4da0180142697a475" integrity sha512-Pkrrm8NjyQ8yVt8Am9M+yUt74zE3iokhzbG1bFVNjLB92vwM71hf40RkEsryg98BujhVOncKm/C1xROxZ030LQ== -minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -23681,11 +23416,6 @@ on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -on-net-listen@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/on-net-listen/-/on-net-listen-1.1.2.tgz#671e55a81c910fa7e5b1e4d506545e9ea0f2e11c" - integrity sha512-y1HRYy8s/RlcBvDUwKXSmkODMdx4KSuIvloCnQYJ2LdBBC1asY4HtfhXwe3UWknLakATZDnbzht2Ijw3M1EqFg== - once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -25414,7 +25144,7 @@ prettier@^3.6.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== -pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: +pretty-bytes@^5.3.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== @@ -26348,11 +26078,6 @@ rehype@^12.0.1: rehype-stringify "^9.0.0" unified "^10.0.0" -reinterval@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/reinterval/-/reinterval-1.1.0.tgz#3361ecfa3ca6c18283380dd0bb9546f390f5ece7" - integrity sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ== - relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" @@ -26676,11 +26401,6 @@ retext@^8.1.0: retext-stringify "^3.0.0" unified "^10.0.0" -retimer@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/retimer/-/retimer-3.0.0.tgz#98b751b1feaf1af13eb0228f8ea68b8f9da530df" - integrity sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA== - retry-request@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.1.3.tgz#d5f74daf261372cff58d08b0a1979b4d7cab0fde" @@ -29000,11 +28720,6 @@ tildify@2.0.0: resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a" integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== -timestring@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/timestring/-/timestring-6.0.0.tgz#b0c7c331981ecf2066ce88bcfb8ee3ae32e7a0f6" - integrity sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA== - tiny-glob@0.2.9, tiny-glob@^0.2.9: version "0.2.9" resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" @@ -29672,7 +29387,7 @@ undici@7.18.2: resolved "https://registry.yarnpkg.com/undici/-/undici-7.18.2.tgz#6cf724ef799a67d94fd55adf66b1e184176efcdf" integrity sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw== -undici@^5.25.4, undici@^5.28.5: +undici@^5.25.4: version "5.29.0" resolved "https://registry.yarnpkg.com/undici/-/undici-5.29.0.tgz#419595449ae3f2cdcba3580a2e8903399bd1f5a3" integrity sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg== @@ -30183,11 +29898,6 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid-parse@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b" - integrity sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A== - uuid-v4@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/uuid-v4/-/uuid-v4-0.1.0.tgz#62d7b310406f6cecfea1528c69f1e8e0bcec5a3a"