-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(core): clear up integrations on dispose #20407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -212,6 +212,14 @@ export abstract class Client<O extends ClientOptions = ClientOptions> { | |
|
|
||
| protected _promiseBuffer: PromiseBuffer<unknown>; | ||
|
|
||
| /** | ||
| * Cleanup functions to call on dispose. | ||
| * | ||
| * NOTE: These callbacks are only invoked by subclasses whose `dispose()` implementation runs them | ||
| * (currently only `ServerRuntimeClient`). The base `Client.dispose()` is a no-op and will not run them. | ||
| */ | ||
| protected _disposeCallbacks: (() => void)[]; | ||
|
|
||
| /** | ||
| * Initializes this client instance. | ||
| * | ||
|
|
@@ -224,6 +232,7 @@ export abstract class Client<O extends ClientOptions = ClientOptions> { | |
| this._outcomes = {}; | ||
| this._hooks = {}; | ||
| this._eventProcessors = []; | ||
| this._disposeCallbacks = []; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. q: This will only be cleaned up in the server runtime client, should we at least document this here? Might be confusing otherwise
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point of adding it there. I'll add it there |
||
| this._promiseBuffer = makePromiseBuffer(options.transportOptions?.bufferSize ?? DEFAULT_TRANSPORT_BUFFER_SIZE); | ||
|
|
||
| if (options.dsn) { | ||
|
|
@@ -1169,10 +1178,26 @@ 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: Registered callbacks are only executed by subclasses whose `dispose()` implementation | ||
| * runs them. At the moment that is only `ServerRuntimeClient` (and clients extending it). On the | ||
| * base `Client` (e.g. the browser client), `dispose()` is a no-op, so callbacks registered here | ||
| * will never be invoked. | ||
| */ | ||
| public registerCleanup(callback: () => void): void { | ||
| this._disposeCallbacks.push(callback); | ||
| } | ||
|
|
||
| /** | ||
| * 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 { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
| } | ||
| } | ||
| }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Array splice during for...of iteration may skip handlersLow Severity The new unsubscribe function uses Additional Locations (1)Triggered by project rule: PR Review Guidelines for Cursor Bot Reviewed by Cursor Bugbot for commit a589811. Configure here.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It shouldn't happen that it is called from somewhere else. |
||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ const _conversationIdIntegration = (() => { | |
| return { | ||
| name: INTEGRATION_NAME, | ||
| setup(client: Client) { | ||
| client.on('spanStart', (span: Span) => { | ||
| const unsubscribe = client.on('spanStart', (span: Span) => { | ||
| const scopeData = getCurrentScope().getScopeData(); | ||
| const isolationScopeData = getIsolationScope().getScopeData(); | ||
|
|
||
|
|
@@ -32,6 +32,8 @@ const _conversationIdIntegration = (() => { | |
| span.setAttribute(GEN_AI_CONVERSATION_ID_ATTRIBUTE, conversationId); | ||
| } | ||
| }); | ||
|
|
||
| client.registerCleanup(unsubscribe); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant cleanup registration for per-client hooksLow Severity The Reviewed by Cursor Bugbot for commit 295ad22. Configure here. |
||
| }, | ||
| }; | ||
| }) satisfies IntegrationFn; | ||
|
|
||
| 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', () => { | ||
|
|
@@ -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); | ||
| }); | ||
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing integration or E2E test for feat PRLow Severity This Additional Locations (1)Triggered by project rule: PR Review Guidelines for Cursor Bot Reviewed by Cursor Bugbot for commit 295ad22. Configure here. |
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bundle size increases in browser packages
Low Severity
Multiple browser package size limits were increased by 0.5–1 KB across several entries (ESM bundles and CDN bundles). This is flagged because the project rules require flagging large bundle size increases in browser packages even when they're unavoidable. The increases stem from adding cleanup infrastructure (
registerCleanup,_disposeCallbacks, unsubscribe return values) that primarily benefits server-side runtimes like Cloudflare.Additional Locations (1)
.size-limit.js#L193-L224Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit 295ad22. Configure here.