Skip to content

Conversation

@joalves
Copy link

@joalves joalves commented Jan 6, 2026

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. The navigator.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 updated Context.publish() to accept a useBeacon option.

Changes

Client (src/client.ts)

  • publishBeacon() - New method that uses navigator.sendBeacon()
    • Includes auth fields in request body: apiKey, agent, environment, application, applicationVersion
    • Returns boolean indicating if beacon was queued successfully
    • Falls back gracefully if navigator.sendBeacon is not available

Publisher (src/publisher.ts)

  • PublishOptions - New type extending ClientRequestOptions with useBeacon flag
  • Updated publish() - Checks useBeacon flag and uses publishBeacon() with fallback

Context (src/context.ts)

  • Updated publish() - Now accepts PublishOptions instead of ClientRequestOptions
  • Updated _flush() - Passes PublishOptions through to publisher

Usage

// Use sendBeacon for reliable tracking during page unload
await context.publish({ useBeacon: true });

// Regular publish (backward compatible)
await context.publish();

Behavior

When useBeacon: true is specified:

  1. Attempts to use navigator.sendBeacon() if available
  2. Falls back to regular fetch-based publish if:
    • sendBeacon is not available (e.g., Node.js environment)
    • sendBeacon returns false (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 handling
  • publishBeacon returns false when sendBeacon is not a function - Graceful degradation
  • publishBeacon sends beacon with auth in body - Verifies auth fields included in body
  • publishBeacon includes goals, exposures, and attributes - Full payload support
  • publishBeacon excludes empty arrays - Payload optimization
  • publishBeacon returns false when sendBeacon returns false - Queue full handling

Publisher Tests (4 new):

  • should use publishBeacon when useBeacon is true and beacon succeeds - Happy path
  • should fallback to regular publish when useBeacon is true but beacon fails - Fallback logic
  • should use regular publish when useBeacon is false - Explicit opt-out
  • should use regular publish when useBeacon is not specified - Backward compatibility

Context Tests (2 new):

  • should pass useBeacon option to publisher - Options propagation
  • should work with useBeacon option and other request options - Combined options

Coverage

  • Client: 99.07% statement coverage
  • Publisher: 100% coverage
  • Context: 98.44% coverage

Dependencies

Requires collector changes: https://github.com/absmartly/abs/pull/3599

  • Collector must accept auth from request body as fallback

Backward Compatibility

Fully backward compatible

  • Existing context.publish() calls work unchanged
  • useBeacon is optional
  • New functionality is opt-in
  • No breaking changes

Summary by CodeRabbit

  • New Features

    • Added optional beacon-based publishing so events can be sent via the browser beacon API when enabled.
  • Behaviour Changes

    • Publish calls now accept a flag to short‑circuit to the beacon path when requested.
  • Tests

    • Added tests covering beacon success/failure, option propagation and payload inclusion/exclusion.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

Walkthrough

This 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

🐇 I stitched a beacon from moonlit thread,
Sent little packets while I hopped ahead,
If sendBeacon sings, I let them fly,
Otherwise I nudge the old route by,
Hooray — data hops along, light as my tread! ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding sendBeacon support for reliable event tracking during page unload, which is the core feature across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings

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.js

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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.stringify or the Blob constructor 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

📥 Commits

Reviewing files that changed from the base of the PR and between 87abcae and 7dc0e3c.

📒 Files selected for processing (3)
  • src/client.ts
  • src/context.ts
  • src/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 PublishOptions and update method signatures consistently through the call chain. Since PublishOptions extends ClientRequestOptions, existing callers remain unaffected whilst new callers can optionally specify useBeacon.

Also applies to: 251-251, 788-788

src/publisher.ts (2)

14-16: LGTM! Type definition is correct.

PublishOptions correctly extends ClientRequestOptions with an optional useBeacon flag, maintaining backward compatibility.


19-26: LGTM! Beacon fallback logic is well-designed.

The implementation correctly attempts sendBeacon() when useBeacon is true and gracefully falls back to regular publish when:

  • sendBeacon is unavailable (returns false)
  • 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 navigator is undefined) and browsers where sendBeacon is unavailable, returning false to 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 since sendBeacon cannot 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, and attributes mirrors the existing publish() 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
@joalves joalves force-pushed the feature/sendbeacon-support branch from 7dc0e3c to 9138a59 Compare January 6, 2026 17:51
Copy link

@coderabbitai coderabbitai bot left a 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 if body contains circular references or non-serialisable values. Since this method returns a boolean rather than throwing, wrap the serialisation in a try-catch block and return false on 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:

  1. Missing test for publishedAt defaulting: The implementation defaults publishedAt to Date.now() when not provided, but there's no test verifying this behaviour (similar to the existing test at lines 904-940 for regular publish()).

  2. 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 using beforeEach/afterEach or wrapping in try/finally blocks for more robust cleanup.

Suggested test for publishedAt defaulting

Add this test case within the publishBeacon describe 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7dc0e3c and 9138a59.

📒 Files selected for processing (6)
  • src/__tests__/client.test.js
  • src/__tests__/context.test.js
  • src/__tests__/publisher.test.js
  • src/client.ts
  • src/context.ts
  • src/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 useBeacon option is correctly propagated through the context.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 publishBeacon is only called when useBeacon is true and that the fallback mechanism works as expected.

src/context.ts (1)

6-6: LGTM!

The signature updates to use PublishOptions are 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 PublishOptions type and beacon short-circuit logic are well-implemented. The method correctly attempts publishBeacon when useBeacon is true and gracefully falls back to the regular publish path when sendBeacon is unavailable or fails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants