Skip to content

Commit b0fee26

Browse files
feat(audience): scaffold @imtbl/audience-sdk package (#2825)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3cdf46d commit b0fee26

14 files changed

Lines changed: 585 additions & 119 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
extends: ['../../../.eslintrc'],
3+
parserOptions: {
4+
project: './tsconfig.eslint.json',
5+
tsconfigRootDir: __dirname,
6+
},
7+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Config } from 'jest';
2+
3+
const config: Config = {
4+
roots: ['<rootDir>/src'],
5+
moduleDirectories: ['node_modules', 'src'],
6+
testEnvironment: 'jsdom',
7+
transform: {
8+
'^.+\\.(t|j)sx?$': '@swc/jest',
9+
},
10+
moduleNameMapper: {
11+
'^@imtbl/audience-core$': '<rootDir>/../core/src/index.ts',
12+
},
13+
};
14+
15+
export default config;

packages/audience/sdk/package.json

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"name": "@imtbl/audience-sdk",
3+
"description": "Immutable Audience SDK — consent-aware event tracking and identity management",
4+
"version": "0.0.0",
5+
"author": "Immutable",
6+
"bugs": "https://github.com/immutable/ts-immutable-sdk/issues",
7+
"dependencies": {
8+
"@imtbl/audience-core": "workspace:*"
9+
},
10+
"devDependencies": {
11+
"@swc/core": "^1.4.2",
12+
"@swc/jest": "^0.2.37",
13+
"@types/jest": "^29.5.12",
14+
"@types/node": "^22.10.7",
15+
"eslint": "^8.56.0",
16+
"jest": "^29.7.0",
17+
"jest-environment-jsdom": "^29.4.3",
18+
"ts-jest": "^29.1.0",
19+
"tsup": "^8.3.0",
20+
"typescript": "^5.6.2"
21+
},
22+
"engines": {
23+
"node": ">=20.11.0"
24+
},
25+
"exports": {
26+
"development": {
27+
"types": "./src/index.ts",
28+
"browser": "./dist/browser/index.js",
29+
"require": "./dist/node/index.cjs",
30+
"default": "./dist/node/index.js"
31+
},
32+
"default": {
33+
"types": "./dist/types/index.d.ts",
34+
"browser": "./dist/browser/index.js",
35+
"require": "./dist/node/index.cjs",
36+
"default": "./dist/node/index.js"
37+
}
38+
},
39+
"files": ["dist"],
40+
"homepage": "https://github.com/immutable/ts-immutable-sdk#readme",
41+
"main": "dist/node/index.cjs",
42+
"module": "dist/node/index.js",
43+
"browser": "dist/browser/index.js",
44+
"publishConfig": {
45+
"access": "public"
46+
},
47+
"repository": "immutable/ts-immutable-sdk.git",
48+
"scripts": {
49+
"build": "pnpm transpile && pnpm typegen",
50+
"transpile": "tsup src/index.ts --config ../../../tsup.config.js",
51+
"typegen": "tsc --customConditions default --emitDeclarationOnly --outDir dist/types",
52+
"lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0",
53+
"test": "jest --passWithNoTests",
54+
"test:watch": "jest --watch",
55+
"typecheck": "tsc --customConditions development --noEmit --jsx preserve"
56+
},
57+
"type": "module",
58+
"types": "./dist/types/index.d.ts"
59+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// SDK-specific constants.
2+
// Backend endpoints and base URLs come from @imtbl/audience-core.
3+
4+
export const LIBRARY_NAME = '@imtbl/audience-sdk';
5+
// Replaced at build time by esbuild replace plugin
6+
export const LIBRARY_VERSION = '__SDK_VERSION__';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { collectContext } from './context';
2+
import { LIBRARY_NAME, LIBRARY_VERSION } from './config';
3+
4+
describe('collectContext', () => {
5+
it('should return context with SDK library name and version', () => {
6+
const ctx = collectContext();
7+
8+
expect(ctx.library).toBe(LIBRARY_NAME);
9+
expect(ctx.libraryVersion).toBe(LIBRARY_VERSION);
10+
});
11+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { EventContext } from '@imtbl/audience-core';
2+
import { collectContext as coreCollectContext } from '@imtbl/audience-core';
3+
import { LIBRARY_NAME, LIBRARY_VERSION } from './config';
4+
5+
export function collectContext(): EventContext {
6+
return coreCollectContext(LIBRARY_NAME, LIBRARY_VERSION);
7+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { Message } from '@imtbl/audience-core';
2+
import { DebugLogger } from './debug';
3+
4+
describe('DebugLogger', () => {
5+
let logSpy: jest.SpyInstance;
6+
let warnSpy: jest.SpyInstance;
7+
8+
beforeEach(() => {
9+
logSpy = jest.spyOn(console, 'log').mockImplementation();
10+
warnSpy = jest.spyOn(console, 'warn').mockImplementation();
11+
});
12+
13+
afterEach(() => {
14+
logSpy.mockRestore();
15+
warnSpy.mockRestore();
16+
});
17+
18+
const stubMessage: Message = {
19+
type: 'track',
20+
messageId: 'msg-1',
21+
eventTimestamp: '2026-01-01T00:00:00Z',
22+
anonymousId: 'anon-1',
23+
surface: 'web',
24+
context: { library: 'test', libraryVersion: '0.0.0' },
25+
event: 'click',
26+
properties: {},
27+
};
28+
29+
it('should not log when disabled', () => {
30+
const logger = new DebugLogger(false);
31+
logger.logEvent('track', stubMessage);
32+
logger.logFlush(true, 1);
33+
logger.logConsent('none', 'full');
34+
logger.logWarning('test');
35+
36+
expect(logSpy).not.toHaveBeenCalled();
37+
expect(warnSpy).not.toHaveBeenCalled();
38+
});
39+
40+
it('should log events when enabled', () => {
41+
const logger = new DebugLogger(true);
42+
logger.logEvent('track', stubMessage);
43+
44+
expect(logSpy).toHaveBeenCalledWith(
45+
'[Immutable Audience] track',
46+
stubMessage,
47+
);
48+
});
49+
50+
it('should log flush status', () => {
51+
const logger = new DebugLogger(true);
52+
53+
logger.logFlush(true, 5);
54+
expect(logSpy).toHaveBeenCalledWith(
55+
'[Immutable Audience] flush ok (5 messages)',
56+
);
57+
58+
logger.logFlush(false, 3);
59+
expect(logSpy).toHaveBeenCalledWith(
60+
'[Immutable Audience] flush failed (3 messages)',
61+
);
62+
});
63+
64+
it('should log consent transitions', () => {
65+
const logger = new DebugLogger(true);
66+
logger.logConsent('none', 'full');
67+
68+
expect(logSpy).toHaveBeenCalledWith(
69+
'[Immutable Audience] consent none → full',
70+
);
71+
});
72+
73+
it('should log warnings', () => {
74+
const logger = new DebugLogger(true);
75+
logger.logWarning('something went wrong');
76+
77+
expect(warnSpy).toHaveBeenCalledWith(
78+
'[Immutable Audience] something went wrong',
79+
);
80+
});
81+
});

packages/audience/sdk/src/debug.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { ConsentLevel, Message } from '@imtbl/audience-core';
2+
3+
const PREFIX = '[Immutable Audience]';
4+
5+
export class DebugLogger {
6+
private enabled: boolean;
7+
8+
constructor(enabled = false) {
9+
this.enabled = enabled;
10+
}
11+
12+
logEvent(method: string, message: Message): void {
13+
if (!this.enabled) return;
14+
// eslint-disable-next-line no-console
15+
console.log(`${PREFIX} ${method}`, message);
16+
}
17+
18+
logFlush(ok: boolean, count: number): void {
19+
if (!this.enabled) return;
20+
// eslint-disable-next-line no-console
21+
console.log(`${PREFIX} flush ${ok ? 'ok' : 'failed'} (${count} messages)`);
22+
}
23+
24+
logConsent(from: ConsentLevel, to: ConsentLevel): void {
25+
if (!this.enabled) return;
26+
// eslint-disable-next-line no-console
27+
console.log(`${PREFIX} consent ${from}${to}`);
28+
}
29+
30+
logWarning(msg: string): void {
31+
if (!this.enabled) return;
32+
// eslint-disable-next-line no-console
33+
console.warn(`${PREFIX} ${msg}`);
34+
}
35+
}

packages/audience/sdk/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type { AudienceSDKConfig } from './types';
2+
export { DebugLogger } from './debug';
3+
export { collectContext } from './context';

packages/audience/sdk/src/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Environment, ConsentLevel } from '@imtbl/audience-core';
2+
3+
/** Configuration for the Immutable Audience SDK. */
4+
export interface AudienceSDKConfig {
5+
publishableKey: string;
6+
environment: Environment;
7+
/** Defaults to 'none' — no tracking until explicitly opted in. */
8+
consent?: ConsentLevel;
9+
debug?: boolean;
10+
cookieDomain?: string;
11+
flushInterval?: number;
12+
flushSize?: number;
13+
}

0 commit comments

Comments
 (0)