-
Notifications
You must be signed in to change notification settings - Fork 5
feat: Add sendBeacon support for reliable event tracking #49
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: main
Are you sure you want to change the base?
Conversation
WalkthroughThis diff adds beacon-based publishing: a new Client.publishBeacon(params: PublishParams): boolean method that builds a JSON Blob merging params with default client fields and sends it to {endpoint}/context via navigator.sendBeacon. A new PublishOptions type (extends ClientRequestOptions) with optional useBeacon is introduced. Context.publish and Context._flush signatures now accept PublishOptions. ContextPublisher.publish checks requestOptions.useBeacon and short-circuits to sdk.getClient().publishBeacon when set. Tests covering publishBeacon behaviour and propagation of useBeacon through Context and Publisher are added. Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ast-grep (0.40.3)src/__tests__/context.test.jsThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/client.ts (1)
169-173: Consider adding defensive error handling.Whilst unlikely,
JSON.stringifyor theBlobconstructor could throw exceptions with malformed data. Wrapping the beacon call in a try-catch block would make the code more resilient.🔎 Proposed defensive error handling
- const url = `${this._opts.endpoint}/context`; - const blob = new Blob([JSON.stringify(body)], { type: "application/json" }); - - return navigator.sendBeacon(url, blob); + try { + const url = `${this._opts.endpoint}/context`; + const blob = new Blob([JSON.stringify(body)], { type: "application/json" }); + return navigator.sendBeacon(url, blob); + } catch (e) { + return false; + }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/client.tssrc/context.tssrc/publisher.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/client.ts (2)
src/publisher.ts (1)
PublishParams(5-12)src/utils.ts (2)
getApplicationName(3-4)getApplicationVersion(6-7)
src/context.ts (1)
src/publisher.ts (1)
PublishOptions(14-16)
🔇 Additional comments (6)
src/context.ts (1)
6-6: LGTM! Signature updates are backward compatible.The changes correctly import
PublishOptionsand update method signatures consistently through the call chain. SincePublishOptionsextendsClientRequestOptions, existing callers remain unaffected whilst new callers can optionally specifyuseBeacon.Also applies to: 251-251, 788-788
src/publisher.ts (2)
14-16: LGTM! Type definition is correct.
PublishOptionscorrectly extendsClientRequestOptionswith an optionaluseBeaconflag, maintaining backward compatibility.
19-26: LGTM! Beacon fallback logic is well-designed.The implementation correctly attempts
sendBeacon()whenuseBeaconis true and gracefully falls back to regular publish when:
sendBeaconis unavailable (returnsfalse)- The beacon fails to queue (returns
false)This provides robust delivery guarantees whilst leveraging beacon's advantages when available.
src/client.ts (3)
136-138: LGTM! Correct feature detection.The check properly handles both Node.js/SSR environments (where
navigatoris undefined) and browsers wheresendBeaconis unavailable, returningfalseto trigger the fallback path.
140-155: LGTM! Body construction correctly includes authentication fields.The method properly merges authentication fields (
apiKey,agent,environment,application,applicationVersion) into the request body sincesendBeaconcannot set custom headers. This coordinates with the collector changes mentioned in the PR description.
157-167: LGTM! Optional array handling is consistent.The logic for conditionally including
goals,exposures, andattributesmirrors the existingpublish()method (lines 116-126), maintaining consistency across both delivery mechanisms.
Enable navigator.sendBeacon() for reliable event publishing during page unload.
Changes:
- Client: Add publishBeacon() method that includes auth in request body
- Publisher: Add PublishOptions type with useBeacon flag
- Publisher: Check useBeacon flag and use sendBeacon with fallback to regular publish
- Context: Update publish() signature to accept PublishOptions
Usage:
await context.publish({ useBeacon: true });
When useBeacon is true:
- Attempts to use navigator.sendBeacon() if available
- Falls back to regular fetch-based publish if sendBeacon fails or is unavailable
- Includes auth fields (apiKey, agent, environment, application, applicationVersion) in request body
Tests:
- All 261 SDK tests pass
- Maintains full backward compatibility
7dc0e3c to
9138a59
Compare
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.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/client.ts (1)
135-173: Add error handling for JSON serialisation.The
JSON.stringify()call on line 170 could throw ifbodycontains circular references or non-serialisable values. Since this method returns a boolean rather than throwing, wrap the serialisation in a try-catch block and returnfalseon error to maintain the graceful degradation pattern.🔎 Proposed fix
publishBeacon(params: PublishParams): boolean { if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") { return false; } const body: PublishParams & { apiKey?: string; agent?: string; environment?: string; application?: string; applicationVersion?: number; } = { units: params.units, hashed: params.hashed, publishedAt: params.publishedAt || Date.now(), apiKey: this._opts.apiKey, agent: this._opts.agent, environment: this._opts.environment, application: getApplicationName(this._opts.application), applicationVersion: getApplicationVersion(this._opts.application), }; if (Array.isArray(params.goals) && params.goals.length > 0) { body.goals = params.goals; } if (Array.isArray(params.exposures) && params.exposures.length > 0) { body.exposures = params.exposures; } if (Array.isArray(params.attributes) && params.attributes.length > 0) { body.attributes = params.attributes; } const url = `${this._opts.endpoint}/context`; - const blob = new Blob([JSON.stringify(body)], { type: "application/json" }); - - return navigator.sendBeacon(url, blob); + + try { + const blob = new Blob([JSON.stringify(body)], { type: "application/json" }); + return navigator.sendBeacon(url, blob); + } catch (error) { + console.warn("publishBeacon failed:", error); + return false; + } }src/__tests__/client.test.js (1)
980-1144: Comprehensive test coverage for publishBeacon with a few suggestions.The test suite thoroughly covers the beacon functionality including availability checks, payload structure, and return value propagation. Well done!
However, consider adding:
Missing test for
publishedAtdefaulting: The implementation defaultspublishedAttoDate.now()when not provided, but there's no test verifying this behaviour (similar to the existing test at lines 904-940 for regularpublish()).Navigator cleanup robustness: Each test manually restores
global.navigator, but if a test throws unexpectedly before the cleanup line executes, subsequent tests could be affected. Consider usingbeforeEach/afterEachor wrapping in try/finally blocks for more robust cleanup.Suggested test for publishedAt defaulting
Add this test case within the
publishBeacondescribe block:it("defaults publishedAt to current time when not provided", async () => { const client = new Client(clientOptions); const sendBeaconMock = jest.fn().mockReturnValue(true); const originalNavigator = global.navigator; const mockNow = 9999999999; jest.spyOn(Date, "now").mockReturnValue(mockNow); global.navigator = { sendBeacon: sendBeaconMock, }; const result = client.publishBeacon({ units, hashed: true, }); expect(result).toBe(true); const callArgs = sendBeaconMock.mock.calls[0]; const blob = callArgs[1]; const text = await blob.text(); const payload = JSON.parse(text); expect(payload.publishedAt).toBe(mockNow); global.navigator = originalNavigator; Date.now.mockRestore(); });Suggested refactor for robust cleanup
Refactor the test suite to use
beforeEach/afterEach:describe("publishBeacon", () => { let originalNavigator; beforeEach(() => { originalNavigator = global.navigator; }); afterEach(() => { global.navigator = originalNavigator; }); it("returns false when navigator is undefined", () => { const client = new Client(clientOptions); delete global.navigator; const result = client.publishBeacon({ units, hashed: true, publishedAt, }); expect(result).toBe(false); }); // ... rest of tests without manual cleanup });
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/__tests__/client.test.jssrc/__tests__/context.test.jssrc/__tests__/publisher.test.jssrc/client.tssrc/context.tssrc/publisher.ts
🧰 Additional context used
🧬 Code graph analysis (4)
src/__tests__/context.test.js (1)
src/context.ts (1)
publisher(243-245)
src/__tests__/publisher.test.js (1)
src/publisher.ts (1)
ContextPublisher(18-28)
src/client.ts (2)
src/publisher.ts (1)
PublishParams(5-12)src/utils.ts (2)
getApplicationName(3-4)getApplicationVersion(6-7)
src/context.ts (1)
src/publisher.ts (1)
PublishOptions(14-16)
🔇 Additional comments (4)
src/__tests__/context.test.js (1)
3278-3308: LGTM!The tests properly verify that the
useBeaconoption is correctly propagated through thecontext.publish()method to the underlying publisher, both when used alone and in combination with other request options.src/__tests__/publisher.test.js (1)
56-126: LGTM!Comprehensive test coverage for the beacon publishing logic, including the short-circuit path when sendBeacon succeeds, fallback when it fails, and default behaviour. The tests properly verify that
publishBeaconis only called whenuseBeaconis true and that the fallback mechanism works as expected.src/context.ts (1)
6-6: LGTM!The signature updates to use
PublishOptionsare type-safe and backward-compatible, properly enabling the beacon publishing feature whilst maintaining the existing API contract.Also applies to: 251-251, 788-788
src/publisher.ts (1)
14-27: LGTM!The
PublishOptionstype and beacon short-circuit logic are well-implemented. The method correctly attemptspublishBeaconwhenuseBeaconis true and gracefully falls back to the regular publish path when sendBeacon is unavailable or fails.
Summary
Enables
navigator.sendBeacon()support for reliable event publishing during page unload, working in conjunction with the collector changes in https://github.com/absmartly/abs/pull/3599.Problem
When users navigate away or close a tab, regular
fetch()requests can be cancelled before completing, resulting in lost tracking data. Thenavigator.sendBeacon()API is specifically designed to reliably send data during page unload, but it cannot set custom headers.Solution
Added
publishBeacon()method to the Client class that includes authentication in the request body instead of headers, and updatedContext.publish()to accept auseBeaconoption.Changes
Client (
src/client.ts)publishBeacon()- New method that usesnavigator.sendBeacon()apiKey,agent,environment,application,applicationVersionbooleanindicating if beacon was queued successfullynavigator.sendBeaconis not availablePublisher (
src/publisher.ts)PublishOptions- New type extendingClientRequestOptionswithuseBeaconflagpublish()- ChecksuseBeaconflag and usespublishBeacon()with fallbackContext (
src/context.ts)publish()- Now acceptsPublishOptionsinstead ofClientRequestOptions_flush()- PassesPublishOptionsthrough to publisherUsage
Behavior
When
useBeacon: trueis specified:navigator.sendBeacon()if availablesendBeaconis not available (e.g., Node.js environment)sendBeaconreturnsfalse(queue full)Test Coverage
✅ All 273 SDK tests pass (12 new tests added)
New Tests
Client Tests (6 new):
publishBeacon returns false when navigator is undefined- Node.js environment handlingpublishBeacon returns false when sendBeacon is not a function- Graceful degradationpublishBeacon sends beacon with auth in body- Verifies auth fields included in bodypublishBeacon includes goals, exposures, and attributes- Full payload supportpublishBeacon excludes empty arrays- Payload optimizationpublishBeacon returns false when sendBeacon returns false- Queue full handlingPublisher Tests (4 new):
should use publishBeacon when useBeacon is true and beacon succeeds- Happy pathshould fallback to regular publish when useBeacon is true but beacon fails- Fallback logicshould use regular publish when useBeacon is false- Explicit opt-outshould use regular publish when useBeacon is not specified- Backward compatibilityContext Tests (2 new):
should pass useBeacon option to publisher- Options propagationshould work with useBeacon option and other request options- Combined optionsCoverage
Dependencies
Requires collector changes: https://github.com/absmartly/abs/pull/3599
Backward Compatibility
✅ Fully backward compatible
context.publish()calls work unchangeduseBeaconis optionalSummary by CodeRabbit
New Features
Behaviour Changes
Tests
✏️ Tip: You can customize this high-level summary in your review settings.