Skip to content

Commit 5532e38

Browse files
committed
feat: audience tracking package
1 parent 0904781 commit 5532e38

23 files changed

Lines changed: 1334 additions & 28 deletions

.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/game-page-team
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+
};

packages/audience/sdk/README.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# @imtbl/audience
2+
3+
Player analytics for game studios on Immutable. Track events, identify players, and link identities across platforms — so you can build a complete picture of every player across web, social, and in-game surfaces.
4+
5+
## Quick Start
6+
7+
```ts
8+
import { Audience, AudienceEvent, IdentityProvider } from '@imtbl/audience';
9+
10+
const audience = new Audience({ publishableKey: 'pk_imx_...', environment: 'sandbox' });
11+
12+
audience.identify(IdentityProvider.Steam, '76561198012345');
13+
audience.track(AudienceEvent.Purchase, { currency: 'USD', value: 9.99 });
14+
```
15+
16+
That's it. Events are batched and sent automatically.
17+
18+
## API
19+
20+
### `new Audience(config)`
21+
22+
| Option | Type | Required | Description |
23+
|---|---|---|---|
24+
| `publishableKey` | `string` | Yes | Publishable API key from [Immutable Hub](https://hub.immutable.com) |
25+
| `environment` | `'dev' \| 'sandbox' \| 'production'` | Yes | Immutable environment |
26+
27+
### `audience.track(event, properties)`
28+
29+
Record a predefined player event. TypeScript enforces correct properties per event.
30+
31+
```ts
32+
audience.track(AudienceEvent.LevelReached, { level: 10, characterClass: 'mage' });
33+
audience.track(AudienceEvent.Spend, { currency: 'GOLD', value: 500, itemName: 'Sword of Fire' });
34+
```
35+
36+
### `audience.identify(provider, uid, traits?)`
37+
38+
Tell us **who** this player is. All subsequent `track()` calls are associated with this user.
39+
40+
```ts
41+
audience.identify(IdentityProvider.Passport, 'abc-123', { email: 'player@example.com' });
42+
audience.identify(IdentityProvider.Steam, '76561198012345');
43+
```
44+
45+
Call `identify()` when the player logs in or you first learn who they are.
46+
47+
### `audience.alias(from, to)`
48+
49+
Link two identities so they are merged into one player profile.
50+
51+
```ts
52+
audience.alias(
53+
{ provider: IdentityProvider.Steam, uid: '76561198012345' },
54+
{ provider: IdentityProvider.Passport, uid: 'abc-123' },
55+
);
56+
```
57+
58+
### `audience.reset()`
59+
60+
Clear the current identity. Call on logout.
61+
62+
---
63+
64+
## Supported Identity Providers
65+
66+
| Provider | Enum | Example uid |
67+
|---|---|---|
68+
| Immutable Passport | `IdentityProvider.Passport` | `'abc-123-uuid'` |
69+
| Steam | `IdentityProvider.Steam` | `'76561198012345'` |
70+
| Epic Games | `IdentityProvider.Epic` | `'epic-account-id'` |
71+
| PlayStation | `IdentityProvider.PlayStation` | `'psn-account-id'` |
72+
| Xbox | `IdentityProvider.Xbox` | `'xbox-user-id'` |
73+
| Nintendo | `IdentityProvider.Nintendo` | `'nintendo-account-id'` |
74+
| Google | `IdentityProvider.Google` | `'google-uid'` |
75+
| Apple | `IdentityProvider.Apple` | `'apple-uid'` |
76+
| Discord | `IdentityProvider.Discord` | `'discord-user-id'` |
77+
| Email | `IdentityProvider.Email` | `'player@example.com'` |
78+
| Custom | `IdentityProvider.Custom` | any string — use when no other provider fits |
79+
80+
---
81+
82+
## Identity: `identify` vs `alias`
83+
84+
| Method | What it does |
85+
|---|---|
86+
| `identify(provider, uid)` | "The current player is **this person**" — call on every login |
87+
| `alias(from, to)` | "These two IDs are **the same person**" — call when you know both |
88+
89+
### Decision tree
90+
91+
```
92+
Player logs in
93+
94+
├─ Does your game use Passport login?
95+
│ │
96+
│ ├─ YES ─── identify(IdentityProvider.Passport, uid)
97+
│ │ │
98+
│ │ └─ Player also has another platform ID (Steam, Epic, etc.)?
99+
│ │ │
100+
│ │ ├─ YES ─── alias({ provider: Steam, uid }, { provider: Passport, uid })
101+
│ │ └─ NO ─── done
102+
│ │
103+
│ └─ NO ──── identify(IdentityProvider.Steam, uid) // or whichever provider
104+
│ └─ done
105+
106+
└─ Player hasn't logged in yet?
107+
└─ Just call track() — when identify() is called later,
108+
prior anonymous activity is attributed to that player.
109+
```
110+
111+
### Example A: Game uses Passport + Steam
112+
113+
```ts
114+
audience.identify(IdentityProvider.Passport, 'abc-123');
115+
audience.track(AudienceEvent.SignIn, { method: 'passport' });
116+
117+
// Player connects Steam in settings
118+
audience.alias(
119+
{ provider: IdentityProvider.Steam, uid: '76561198012345' },
120+
{ provider: IdentityProvider.Passport, uid: 'abc-123' },
121+
);
122+
```
123+
124+
### Example B: Game uses Steam only (no Passport)
125+
126+
```ts
127+
audience.identify(IdentityProvider.Steam, '76561198012345');
128+
audience.track(AudienceEvent.SignIn, { method: 'steam' });
129+
130+
// That's it. No alias() needed.
131+
```
132+
133+
### Example C: Anonymous visitor → known player
134+
135+
```ts
136+
// Before login — events are captured anonymously
137+
audience.track(AudienceEvent.WishlistAdd, { gameId: 'my-game', source: 'twitter' });
138+
139+
// Player signs up
140+
audience.identify(IdentityProvider.Passport, 'abc-123', { email: 'player@example.com' });
141+
audience.track(AudienceEvent.SignUp, { method: 'passport' });
142+
```
143+
144+
### TL;DR
145+
146+
- **Always** call `identify()` on login with the appropriate `IdentityProvider`.
147+
- **Only** call `alias()` if your game knows two IDs for the same player.
148+
- If you only have one ID, you're done.
149+
150+
---
151+
152+
## Predefined Events
153+
154+
### Web / Distribution
155+
156+
| Event | Required Properties | Optional Properties |
157+
|---|---|---|
158+
| `SignUp` || `method` |
159+
| `SignIn` || `method` |
160+
| `WishlistAdd` | `gameId` | `source` |
161+
| `WishlistRemove` | `gameId` ||
162+
| `Purchase` | `currency`, `value` | `itemId`, `itemName`, `quantity` |
163+
164+
### In-Game
165+
166+
| Event | Required Properties | Optional Properties |
167+
|---|---|---|
168+
| `SessionStart` || `sessionId` |
169+
| `SessionEnd` || `sessionId`, `duration` (seconds) |
170+
| `LevelReached` | `level` | `characterClass` |
171+
| `Spend` | `currency`, `value` | `itemId`, `itemName` |
172+
| `TutorialComplete` || `stepNumber` |
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
};
11+
12+
export default config;

packages/audience/sdk/package.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"name": "@imtbl/audience",
3+
"description": "Audience analytics SDK for Immutable — track player events, identity and attribution",
4+
"version": "0.0.0",
5+
"author": "Immutable",
6+
"bugs": "https://github.com/immutable/ts-immutable-sdk/issues",
7+
"dependencies": {},
8+
"devDependencies": {
9+
"@swc/core": "^1.4.2",
10+
"@swc/jest": "^0.2.37",
11+
"@types/jest": "^29.5.12",
12+
"@types/node": "^22.10.7",
13+
"eslint": "^8.56.0",
14+
"jest": "^29.7.0",
15+
"jest-environment-jsdom": "^29.4.3",
16+
"ts-jest": "^29.1.0",
17+
"tsup": "^8.3.0",
18+
"typescript": "^5.6.2"
19+
},
20+
"engines": {
21+
"node": ">=20.11.0"
22+
},
23+
"exports": {
24+
"development": {
25+
"types": "./src/index.ts",
26+
"browser": "./dist/browser/index.js",
27+
"require": "./dist/node/index.cjs",
28+
"default": "./dist/node/index.js"
29+
},
30+
"default": {
31+
"types": "./dist/types/index.d.ts",
32+
"browser": "./dist/browser/index.js",
33+
"require": "./dist/node/index.cjs",
34+
"default": "./dist/node/index.js"
35+
}
36+
},
37+
"files": [
38+
"dist"
39+
],
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+
"pack:root": "pnpm pack --pack-destination $(dirname $(pnpm root -w))",
53+
"lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0",
54+
"test": "jest",
55+
"test:watch": "jest --watch",
56+
"typecheck": "tsc --customConditions default --noEmit --jsx preserve"
57+
},
58+
"type": "module",
59+
"types": "./dist/types/index.d.ts"
60+
}

0 commit comments

Comments
 (0)