Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
gzip: true,
limit: '83 KB',
limit: '84 KB',
Comment thread
cursor[bot] marked this conversation as resolved.
},
{
name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags',
Expand Down Expand Up @@ -138,7 +138,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'metrics', 'logger'),
gzip: true,
limit: '28 KB',
limit: '29 KB',
},
// React SDK (ESM)
{
Expand Down Expand Up @@ -191,13 +191,13 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing)',
path: createCDNPath('bundle.tracing.min.js'),
gzip: true,
limit: '46.5 KB',
limit: '47 KB',
},
{
name: 'CDN Bundle (incl. Logs, Metrics)',
path: createCDNPath('bundle.logs.metrics.min.js'),
gzip: true,
limit: '30 KB',
limit: '31 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Logs, Metrics)',
Expand All @@ -209,19 +209,19 @@ module.exports = [
name: 'CDN Bundle (incl. Replay, Logs, Metrics)',
path: createCDNPath('bundle.replay.logs.metrics.min.js'),
gzip: true,
limit: '69 KB',
limit: '70 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay)',
path: createCDNPath('bundle.tracing.replay.min.js'),
gzip: true,
limit: '83.5 KB',
limit: '84 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Logs, Metrics)',
path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'),
gzip: true,
limit: '84.5 KB',
limit: '85 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Feedback)',
Expand Down Expand Up @@ -283,21 +283,21 @@ module.exports = [
path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'),
gzip: false,
brotli: false,
limit: '258.5 KB',
limit: '259 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed',
path: createCDNPath('bundle.tracing.replay.feedback.min.js'),
gzip: false,
brotli: false,
limit: '268 KB',
limit: '269 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed',
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: false,
brotli: false,
limit: '271.5 KB',
limit: '272 KB',
},
// Next.js SDK (ESM)
{
Expand Down
17 changes: 16 additions & 1 deletion packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1169,10 +1169,25 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
return {};
}

/**
* Register a cleanup function to be called when the client is disposed.
* This is useful for integrations that need to clean up global state.
*
* NOTE: This is a no-op in the base `Client` class. Subclasses like `ServerRuntimeClient`
* override this method to actually register and execute cleanup callbacks.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public registerCleanup(callback: () => void): void {
// No-op in base class - subclasses override to implement cleanup registration
}

/**
* Disposes of the client and releases all resources.
*
* Subclasses should override this method to clean up their own resources.
* Subclasses should override this method to clean up their own resources, including invoking
* any callbacks registered via {@link Client.registerCleanup}. The base implementation is a
* no-op and does NOT execute registered cleanup callbacks.
*
* After calling dispose(), the client should not be used anymore.
*/
public dispose(): void {
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/instrument/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import { addHandler, maybeInstrument, triggerHandlers } from './handlers';

/**
* Add an instrumentation handler for when a console.xxx method is called.
* Returns a function to remove the handler.
*
* Use at your own risk, this might break without changelog notice, only used internally.
* @hidden
*/
export function addConsoleInstrumentationHandler(handler: (data: HandlerDataConsole) => void): void {
export function addConsoleInstrumentationHandler(handler: (data: HandlerDataConsole) => void): () => void {
Comment thread
JPeer264 marked this conversation as resolved.
const type = 'console';
addHandler(type, handler);
const removeHandler = addHandler(type, handler);
maybeInstrument(type, instrumentConsole);
return removeHandler;
}

function instrumentConsole(): void {
Expand Down
12 changes: 8 additions & 4 deletions packages/core/src/instrument/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,35 @@ type FetchResource = string | { toString(): string } | { url: string };
* Add an instrumentation handler for when a fetch request happens.
* The handler function is called once when the request starts and once when it ends,
* which can be identified by checking if it has an `endTimestamp`.
* Returns a function to remove the handler.
*
* Use at your own risk, this might break without changelog notice, only used internally.
* @hidden
*/
export function addFetchInstrumentationHandler(
handler: (data: HandlerDataFetch) => void,
skipNativeFetchCheck?: boolean,
): void {
): () => void {
const type = 'fetch';
addHandler(type, handler);
const removeHandler = addHandler(type, handler);
maybeInstrument(type, () => instrumentFetch(undefined, skipNativeFetchCheck));
return removeHandler;
}

/**
* Add an instrumentation handler for long-lived fetch requests, like consuming server-sent events (SSE) via fetch.
* The handler will resolve the request body and emit the actual `endTimestamp`, so that the
* span can be updated accordingly.
* Returns a function to remove the handler.
*
* Only used internally
* @hidden
*/
export function addFetchEndInstrumentationHandler(handler: (data: HandlerDataFetch) => void): void {
export function addFetchEndInstrumentationHandler(handler: (data: HandlerDataFetch) => void): () => void {
const type = 'fetch-body-resolved';
addHandler(type, handler);
const removeHandler = addHandler(type, handler);
maybeInstrument(type, () => instrumentFetch(streamHandler));
return removeHandler;
}

function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNativeFetchCheck: boolean = false): void {
Expand Down
14 changes: 12 additions & 2 deletions packages/core/src/instrument/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,20 @@ export type InstrumentHandlerCallback = (data: any) => void;
const handlers: { [key in InstrumentHandlerType]?: InstrumentHandlerCallback[] } = {};
const instrumented: { [key in InstrumentHandlerType]?: boolean } = {};

/** Add a handler function. */
export function addHandler(type: InstrumentHandlerType, handler: InstrumentHandlerCallback): void {
/** Add a handler function. Returns a function to remove the handler. */
export function addHandler(type: InstrumentHandlerType, handler: InstrumentHandlerCallback): () => void {
handlers[type] = handlers[type] || [];
handlers[type].push(handler);

return () => {
const typeHandlers = handlers[type];
if (typeHandlers) {
const index = typeHandlers.indexOf(handler);
if (index !== -1) {
typeHandlers.splice(index, 1);
}
}
};
Comment thread
JPeer264 marked this conversation as resolved.
}

/**
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/integrations/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ export const consoleIntegration = defineIntegration((options: Partial<ConsoleInt
return {
name: INTEGRATION_NAME,
setup(client) {
addConsoleInstrumentationHandler(({ args, level }) => {
const unsubscribe = addConsoleInstrumentationHandler(({ args, level }) => {
if (getClient() !== client || !levels.has(level)) {
return;
}

addConsoleBreadcrumb(level, args);
});

client.registerCleanup(unsubscribe);
},
};
});
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/logs/console-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const _consoleLoggingIntegration = ((options: Partial<CaptureConsoleOptions> = {
return;
}

addConsoleInstrumentationHandler(({ args, level }) => {
const unsubscribe = addConsoleInstrumentationHandler(({ args, level }) => {
if (getClient() !== client || !levels.includes(level)) {
return;
}
Expand Down Expand Up @@ -66,6 +66,8 @@ const _consoleLoggingIntegration = ((options: Partial<CaptureConsoleOptions> = {
attributes,
});
});

client.registerCleanup(unsubscribe);
},
};
}) satisfies IntegrationFn;
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/server-runtime-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface ServerRuntimeClientOptions extends ClientOptions<BaseTransportO
export class ServerRuntimeClient<
O extends ClientOptions & ServerRuntimeClientOptions = ServerRuntimeClientOptions,
> extends Client<O> {
private _disposeCallbacks: (() => void)[] = [];

/**
* Creates a new Edge SDK instance.
* @param options Configuration options for this SDK.
Expand Down Expand Up @@ -154,6 +156,13 @@ export class ServerRuntimeClient<
return id;
}

/**
* @inheritDoc
*/
public override registerCleanup(callback: () => void): void {
this._disposeCallbacks.push(callback);
}

/**
* Disposes of the client and releases all resources.
*
Expand All @@ -168,6 +177,16 @@ export class ServerRuntimeClient<
public override dispose(): void {
DEBUG_BUILD && debug.log('Disposing client...');

// Run all registered cleanup callbacks
for (const callback of this._disposeCallbacks) {
try {
callback();
} catch {
// Ignore errors in cleanup callbacks
}
}
this._disposeCallbacks.length = 0;

for (const hookName of Object.keys(this._hooks)) {
this._hooks[hookName]?.clear();
}
Expand Down
99 changes: 97 additions & 2 deletions packages/core/test/lib/instrument/handlers.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { describe, test } from 'vitest';
import { maybeInstrument } from '../../../src/instrument/handlers';
import { afterEach, describe, expect, test, vi } from 'vitest';
import {
addHandler,
maybeInstrument,
resetInstrumentationHandlers,
triggerHandlers,
} from '../../../src/instrument/handlers';

afterEach(() => {
resetInstrumentationHandlers();
});

describe('maybeInstrument', () => {
test('does not throw when instrumenting fails', () => {
Expand All @@ -12,3 +21,89 @@ describe('maybeInstrument', () => {
maybeInstrument('xhr', undefined as any);
});
});

describe('addHandler', () => {
test('returns an unsubscribe function', () => {
const handler = vi.fn();
const unsubscribe = addHandler('fetch', handler);

expect(typeof unsubscribe).toBe('function');
});

test('handler is called when triggerHandlers is invoked', () => {
const handler = vi.fn();
addHandler('fetch', handler);

triggerHandlers('fetch', { url: 'https://example.com' });

expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith({ url: 'https://example.com' });
});

test('unsubscribe removes the handler', () => {
const handler = vi.fn();
const unsubscribe = addHandler('fetch', handler);

triggerHandlers('fetch', { test: 1 });
expect(handler).toHaveBeenCalledTimes(1);

unsubscribe();

triggerHandlers('fetch', { test: 2 });
expect(handler).toHaveBeenCalledTimes(1);
});

test('unsubscribe only removes the specific handler', () => {
const handler1 = vi.fn();
const handler2 = vi.fn();

const unsubscribe1 = addHandler('fetch', handler1);
addHandler('fetch', handler2);

triggerHandlers('fetch', { test: 1 });
expect(handler1).toHaveBeenCalledTimes(1);
expect(handler2).toHaveBeenCalledTimes(1);

unsubscribe1();

triggerHandlers('fetch', { test: 2 });
expect(handler1).toHaveBeenCalledTimes(1);
expect(handler2).toHaveBeenCalledTimes(2);
});

test('calling unsubscribe multiple times is safe', () => {
const handler = vi.fn();
const unsubscribe = addHandler('fetch', handler);

unsubscribe();
expect(() => unsubscribe()).not.toThrow();
expect(() => unsubscribe()).not.toThrow();
});

test('unsubscribe works with different handler types', () => {
const consoleHandler = vi.fn();
const fetchHandler = vi.fn();

const unsubscribeConsole = addHandler('console', consoleHandler);
const unsubscribeFetch = addHandler('fetch', fetchHandler);

triggerHandlers('console', { level: 'log' });
triggerHandlers('fetch', { url: 'test' });

expect(consoleHandler).toHaveBeenCalledTimes(1);
expect(fetchHandler).toHaveBeenCalledTimes(1);

unsubscribeConsole();

triggerHandlers('console', { level: 'warn' });
triggerHandlers('fetch', { url: 'test2' });

expect(consoleHandler).toHaveBeenCalledTimes(1);
expect(fetchHandler).toHaveBeenCalledTimes(2);

unsubscribeFetch();

triggerHandlers('fetch', { url: 'test3' });
expect(fetchHandler).toHaveBeenCalledTimes(2);
});
});
Comment thread
JPeer264 marked this conversation as resolved.
Loading
Loading