Skip to content

Commit 0f3193a

Browse files
feat(audience): wire onError callback from AudienceConfig through to core
Exposes AudienceError to web SDK consumers. The core queue and consent manager already accept an onError callback and map TransportError to a public AudienceError via toAudienceError; this commit just forwards AudienceConfig.onError into both positional slots in the Audience constructor, which were previously hard-coded to undefined. Also re-exports AudienceError (runtime) and AudienceErrorCode (type) from the @imtbl/audience package index so ESM consumers can import the error type they need for their onError handler. CDN consumers already have it via window.ImmutableAudience.AudienceError from the CDN bundle work in #2853. Two new sdk.test.ts tests cover the wire-through end-to-end: a flush failure against a mocked backend, and a consent sync failure against a mocked backend. Both verify the AudienceError shape (name, code, status, endpoint) rather than just that the callback fires. Refs SDK-49 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 42565f8 commit 0f3193a

File tree

4 files changed

+79
-3
lines changed

4 files changed

+79
-3
lines changed

packages/audience/sdk/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
export { Audience } from './sdk';
22
export { AudienceEvents } from './events';
3-
export { IdentityType, canTrack, canIdentify } from '@imtbl/audience-core';
3+
export {
4+
IdentityType,
5+
canTrack,
6+
canIdentify,
7+
AudienceError,
8+
} from '@imtbl/audience-core';
49
export type { ImmutableAudienceGlobal } from './cdn';
510
export type { AudienceConfig } from './types';
11+
export type { AudienceErrorCode } from '@imtbl/audience-core';
612
export type {
713
AudienceEventName,
814
EmailAcquiredProperties,

packages/audience/sdk/src/sdk.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,4 +905,65 @@ describe('Audience', () => {
905905
sdk.shutdown();
906906
});
907907
});
908+
909+
describe('onError wire-through', () => {
910+
it('invokes config.onError with an AudienceError when a flush fails', async () => {
911+
mockFetch.mockImplementationOnce(
912+
async (url: string, init?: RequestInit) => {
913+
fetchCalls.push({ url, init: init ?? {} });
914+
return { ok: false, status: 500, json: async () => ({}) };
915+
},
916+
);
917+
918+
const errors: any[] = [];
919+
const sdk = createSDK({
920+
consent: 'anonymous',
921+
onError: (err: any) => errors.push(err),
922+
});
923+
924+
sdk.page();
925+
await sdk.flush();
926+
927+
expect(errors).toHaveLength(1);
928+
expect(errors[0].name).toBe('AudienceError');
929+
expect(errors[0].code).toBe('FLUSH_FAILED');
930+
expect(errors[0].status).toBe(500);
931+
expect(errors[0].endpoint).toContain(INGEST_PATH);
932+
933+
sdk.shutdown();
934+
});
935+
936+
it('invokes config.onError with an AudienceError when consent sync fails', async () => {
937+
mockFetch.mockImplementation(
938+
async (url: string, init?: RequestInit) => {
939+
fetchCalls.push({ url, init: init ?? {} });
940+
if (url.includes(CONSENT_PATH)) {
941+
return { ok: false, status: 503, json: async () => ({}) };
942+
}
943+
return { ok: true, json: async () => ({}) };
944+
},
945+
);
946+
947+
const errors: any[] = [];
948+
const sdk = createSDK({
949+
consent: 'none',
950+
onError: (err: any) => errors.push(err),
951+
});
952+
953+
sdk.setConsent('anonymous');
954+
955+
// Yield for the fire-and-forget consent PUT to resolve
956+
await Promise.resolve();
957+
await Promise.resolve();
958+
await Promise.resolve();
959+
960+
expect(errors).toHaveLength(1);
961+
expect(errors[0].name).toBe('AudienceError');
962+
expect(errors[0].code).toBe('CONSENT_SYNC_FAILED');
963+
expect(errors[0].status).toBe(503);
964+
expect(errors[0].endpoint).toContain(CONSENT_PATH);
965+
966+
sdk.shutdown();
967+
});
968+
});
908969
});

packages/audience/sdk/src/sdk.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export class Audience {
9393
flushIntervalMs: config.flushInterval,
9494
flushSize: config.flushSize,
9595
onFlush: (ok, count) => this.debug.logFlush(ok, count),
96+
onError: config.onError,
9697
staleFilter: (m) => isTimestampValid(m.eventTimestamp),
9798
storagePrefix: '__imtbl_web_',
9899
},
@@ -105,7 +106,7 @@ export class Audience {
105106
this.anonymousId,
106107
consentSource,
107108
consentLevel,
108-
undefined,
109+
config.onError,
109110
config.baseUrl,
110111
);
111112

packages/audience/sdk/src/types.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ConsentLevel } from '@imtbl/audience-core';
1+
import type { AudienceError, ConsentLevel } from '@imtbl/audience-core';
22

33
/** Configuration for the Immutable Web SDK. */
44
export interface AudienceConfig {
@@ -16,4 +16,12 @@ export interface AudienceConfig {
1616
flushSize?: number;
1717
/** Override the default API base URL. */
1818
baseUrl?: string;
19+
/**
20+
* Called when the SDK fails to reach the backend. Receives a structured
21+
* {@link AudienceError} with a machine-readable `code` so studios can
22+
* branch on the failure mode (FLUSH_FAILED, CONSENT_SYNC_FAILED,
23+
* NETWORK_ERROR, VALIDATION_REJECTED). Exceptions thrown from this
24+
* callback are swallowed so a bad handler can't wedge the SDK.
25+
*/
26+
onError?: (err: AudienceError) => void;
1927
}

0 commit comments

Comments
 (0)