Skip to content

Commit a60b578

Browse files
committed
Merge branch 'main' into remove/BSB-6-sdk-starkex-guardian-client
2 parents ab6471d + 911bed4 commit a60b578

26 files changed

Lines changed: 1321 additions & 1 deletion

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package.json @immutable/blockchain-services
2121
/packages/blockchain-data @immutable/activation
2222
/packages/minting-backend @shineli1984
2323
/packages/webhook @shineli1984
24+
/packages/audience @immutable/ped-stream-sdk-integrations-list
2425
/packages/game-bridge @immutable/gamesdk @immutable/blockchain-services
2526
/examples @immutable/devgrowth
2627
**/package.json @immutable/blockchain-services
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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { Config } from 'jest';
2+
3+
const config: Config = {
4+
roots: ['<rootDir>/src'],
5+
moduleDirectories: ['node_modules', 'src'],
6+
moduleNameMapper: { '^@imtbl/(.*)$': '<rootDir>/../../../node_modules/@imtbl/$1/src' },
7+
testEnvironment: 'jsdom',
8+
transform: {
9+
'^.+\\.(t|j)sx?$': '@swc/jest',
10+
},
11+
transformIgnorePatterns: [],
12+
};
13+
14+
export default config;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "@imtbl/audience-core",
3+
"description": "Shared core for Immutable Audience SDKs: transport, queue, cookie, context collection",
4+
"version": "0.0.0",
5+
"author": "Immutable",
6+
"private": true,
7+
"bugs": "https://github.com/immutable/ts-immutable-sdk/issues",
8+
"dependencies": {
9+
"@imtbl/metrics": "workspace:*"
10+
},
11+
"devDependencies": {
12+
"@swc/core": "^1.4.2",
13+
"@swc/jest": "^0.2.37",
14+
"@types/jest": "^29.5.12",
15+
"eslint": "^8.56.0",
16+
"jest": "^29.7.0",
17+
"jest-environment-jsdom": "^29.4.3",
18+
"tsup": "^8.3.0",
19+
"typescript": "^5.6.2"
20+
},
21+
"exports": {
22+
"development": {
23+
"types": "./src/index.ts",
24+
"browser": "./dist/browser/index.js",
25+
"require": "./dist/node/index.cjs",
26+
"default": "./dist/node/index.js"
27+
},
28+
"default": {
29+
"types": "./dist/types/index.d.ts",
30+
"browser": "./dist/browser/index.js",
31+
"require": "./dist/node/index.cjs",
32+
"default": "./dist/node/index.js"
33+
}
34+
},
35+
"files": ["dist"],
36+
"main": "dist/node/index.cjs",
37+
"module": "dist/node/index.js",
38+
"browser": "dist/browser/index.js",
39+
"homepage": "https://github.com/immutable/ts-immutable-sdk#readme",
40+
"repository": "immutable/ts-immutable-sdk.git",
41+
"scripts": {
42+
"build": "pnpm transpile && pnpm typegen",
43+
"transpile": "tsup src/index.ts --config ../../../tsup.config.js",
44+
"typegen": "tsc --customConditions default --emitDeclarationOnly --outDir dist/types",
45+
"lint": "eslint ./src --ext .ts --max-warnings=0",
46+
"test": "jest --passWithNoTests",
47+
"typecheck": "tsc --customConditions default --noEmit"
48+
},
49+
"type": "module",
50+
"types": "./dist/types/index.d.ts"
51+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { getBaseUrl } from './config';
2+
3+
describe('getBaseUrl', () => {
4+
it('returns dev URL', () => {
5+
expect(getBaseUrl('dev')).toBe('https://api.dev.immutable.com');
6+
});
7+
8+
it('returns sandbox URL', () => {
9+
expect(getBaseUrl('sandbox')).toBe('https://api.sandbox.immutable.com');
10+
});
11+
12+
it('returns production URL', () => {
13+
expect(getBaseUrl('production')).toBe('https://api.immutable.com');
14+
});
15+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Environment } from './types';
2+
3+
const BASE_URLS: Record<Environment, string> = {
4+
dev: 'https://api.dev.immutable.com',
5+
sandbox: 'https://api.sandbox.immutable.com',
6+
production: 'https://api.immutable.com',
7+
};
8+
9+
export const INGEST_PATH = '/v1/audience/messages';
10+
export const CONSENT_PATH = '/v1/audience/tracking-consent';
11+
12+
export const FLUSH_INTERVAL_MS = 5_000;
13+
export const FLUSH_SIZE = 20;
14+
15+
export const COOKIE_NAME = 'imtbl_anon_id';
16+
export const SESSION_COOKIE = '_imtbl_sid';
17+
export const COOKIE_MAX_AGE_SECONDS = 365 * 24 * 60 * 60 * 2; // 2 years
18+
19+
export const getBaseUrl = (environment: Environment): string => BASE_URLS[environment];
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { collectContext } from './context';
2+
3+
describe('collectContext', () => {
4+
it('defaults to @imtbl/audience library name', () => {
5+
const ctx = collectContext();
6+
expect(ctx.library).toBe('@imtbl/audience');
7+
expect(ctx.libraryVersion).toBeDefined();
8+
});
9+
10+
it('accepts custom library name and version', () => {
11+
const ctx = collectContext('@imtbl/audience-web-sdk', '1.0.0');
12+
expect(ctx.library).toBe('@imtbl/audience-web-sdk');
13+
expect(ctx.libraryVersion).toBe('1.0.0');
14+
});
15+
16+
it('collects browser signals in jsdom', () => {
17+
const ctx = collectContext();
18+
expect(ctx.userAgent).toBeDefined();
19+
expect(ctx.locale).toBeDefined();
20+
expect(ctx.timezone).toBeDefined();
21+
expect(ctx.screen).toMatch(/^\d+x\d+$/);
22+
});
23+
24+
it('collects page info', () => {
25+
const ctx = collectContext();
26+
expect(ctx.pageUrl).toBeDefined();
27+
expect(ctx.pagePath).toBeDefined();
28+
expect(typeof ctx.pageReferrer).toBe('string');
29+
expect(typeof ctx.pageTitle).toBe('string');
30+
});
31+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { EventContext } from './types';
2+
import { isBrowser } from './utils';
3+
4+
// WARNING: DO NOT CHANGE THE STRING BELOW. IT GETS REPLACED AT BUILD TIME.
5+
const SDK_VERSION = '__SDK_VERSION__';
6+
7+
/**
8+
* Collect browser context for event payloads.
9+
*
10+
* Callers may pass their own library name and version when multiple surfaces
11+
* (web SDK, pixel, Unity, Unreal) share this function and each must identify
12+
* itself. Defaults to '@imtbl/audience' with the build-time SDK version.
13+
*/
14+
export function collectContext(
15+
library = '@imtbl/audience',
16+
version = SDK_VERSION,
17+
): EventContext {
18+
const context: EventContext = {
19+
library,
20+
libraryVersion: version,
21+
};
22+
23+
if (!isBrowser()) return context;
24+
25+
context.userAgent = navigator.userAgent;
26+
context.locale = navigator.language;
27+
context.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
28+
context.screen = `${window.screen.width}x${window.screen.height}`;
29+
context.pageUrl = window.location.href;
30+
context.pagePath = window.location.pathname;
31+
context.pageReferrer = document.referrer;
32+
context.pageTitle = document.title;
33+
34+
return context;
35+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { getOrCreateAnonymousId, getAnonymousId } from './cookie';
2+
import { COOKIE_NAME } from './config';
3+
4+
function clearCookies() {
5+
document.cookie.split(';').forEach((c) => {
6+
const name = c.split('=')[0].trim();
7+
document.cookie = `${name}=; max-age=0; path=/`;
8+
});
9+
}
10+
11+
beforeEach(clearCookies);
12+
13+
describe('getOrCreateAnonymousId', () => {
14+
it('generates a new ID when no cookie exists', () => {
15+
const id = getOrCreateAnonymousId();
16+
expect(id).toBeTruthy();
17+
expect(typeof id).toBe('string');
18+
});
19+
20+
it('persists the ID in a cookie', () => {
21+
const id = getOrCreateAnonymousId();
22+
expect(document.cookie).toContain(`${COOKIE_NAME}=${id}`);
23+
});
24+
25+
it('returns the same ID on subsequent calls', () => {
26+
const first = getOrCreateAnonymousId();
27+
const second = getOrCreateAnonymousId();
28+
expect(second).toBe(first);
29+
});
30+
31+
it('returns an existing cookie value if already set', () => {
32+
document.cookie = `${COOKIE_NAME}=existing-id; path=/`;
33+
expect(getOrCreateAnonymousId()).toBe('existing-id');
34+
});
35+
});
36+
37+
describe('getAnonymousId', () => {
38+
it('returns undefined when no cookie exists', () => {
39+
expect(getAnonymousId()).toBeUndefined();
40+
});
41+
42+
it('returns the cookie value when set', () => {
43+
document.cookie = `${COOKIE_NAME}=test-id; path=/`;
44+
expect(getAnonymousId()).toBe('test-id');
45+
});
46+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { COOKIE_NAME, COOKIE_MAX_AGE_SECONDS } from './config';
2+
import { isBrowser, generateId } from './utils';
3+
4+
export function getCookie(name: string): string | undefined {
5+
if (!isBrowser()) return undefined;
6+
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
7+
return match ? decodeURIComponent(match[1]) : undefined;
8+
}
9+
10+
export function setCookie(name: string, value: string, maxAge: number, domain?: string): void {
11+
if (!isBrowser()) return;
12+
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
13+
const domainAttr = domain ? `; domain=${domain}` : '';
14+
document.cookie = `${name}=${encodeURIComponent(value)}`
15+
+ `; path=/; max-age=${maxAge}; SameSite=Lax${domainAttr}${secure}`;
16+
}
17+
18+
export function deleteCookie(name: string, domain?: string): void {
19+
setCookie(name, '', 0, domain);
20+
}
21+
22+
/**
23+
* Returns the anonymous ID from the shared cookie, creating one if it doesn't exist.
24+
* Both the web SDK and pixel read/write the same cookie so identity stitching
25+
* works across surfaces on the same domain.
26+
*/
27+
export function getOrCreateAnonymousId(domain?: string): string {
28+
const existing = getCookie(COOKIE_NAME);
29+
if (existing) return existing;
30+
31+
const id = generateId();
32+
setCookie(COOKIE_NAME, id, COOKIE_MAX_AGE_SECONDS, domain);
33+
return id;
34+
}
35+
36+
export function getAnonymousId(): string | undefined {
37+
return getCookie(COOKIE_NAME);
38+
}

0 commit comments

Comments
 (0)