@trackstall/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +75 -0
  2. package/index.d.ts +843 -0
  3. package/index.js +2734 -0
  4. package/package.json +53 -0
package/index.d.ts ADDED
@@ -0,0 +1,843 @@
1
+ // Generated by dts-bundle-generator v9.5.1
2
+
3
+ /**
4
+ * Canonical attribution schema shared between the TrackStall SDK (clients that
5
+ * integrate into any app) and the Node/Lambda server. All types here are
6
+ * runtime-agnostic: no Hermes-only APIs, no Node-only APIs.
7
+ *
8
+ * A single `AttributionEvent` fans out to:
9
+ * - Meta SDK (in-app `logEvent`)
10
+ * - TikTok Business SDK (in-app `trackEvent`)
11
+ * - Meta Conversions API (server)
12
+ * - TikTok Events API (server)
13
+ * - TrackStall's own Mongo store (reporting / dashboard)
14
+ *
15
+ * Cross-channel deduplication relies on `eventId` being identical across all
16
+ * destinations — see `eventId.ts`.
17
+ *
18
+ * Multi-tenancy: every event carries an `appId`. The server scopes every read
19
+ * and write by it, and resolves the per-app provider credentials (Meta CAPI
20
+ * token + pixel, TikTok EAPI token + pixel) from it.
21
+ */
22
+ /** ISO 4217 currency code (e.g. "USD", "BRL"). Always upper-case. */
23
+ export type Currency = string;
24
+ /** Runtime platform the event was emitted from. */
25
+ export type Platform = "ios" | "android" | "web";
26
+ /** Origin store of a paid transaction. */
27
+ export type StorePlatform = "apple" | "google" | "stripe" | "web" | "unknown";
28
+ /** Snapshot of App Tracking Transparency status at event time. */
29
+ export type AttStatus = "authorized" | "denied" | "restricted" | "undetermined" | "unavailable";
30
+ /**
31
+ * Canonical event names recognized by TrackStall. These are vendor-neutral;
32
+ * adapters map them to vendor-specific names at the edge (e.g. `subscribe` →
33
+ * `fb_mobile_subscribe` on Meta SDK, `Subscribe` on Meta CAPI, `subscribe` on
34
+ * TikTok Events API).
35
+ *
36
+ * Because TrackStall is multi-tenant and serves arbitrary apps, the event
37
+ * `name` accepts any string too — apps can emit custom events while still
38
+ * getting autocomplete on the canonical set. Custom names pass through to
39
+ * vendor `custom_data` / `properties` and to the dashboard as-is.
40
+ */
41
+ export type CanonicalEventName = "install" | "app_open" | "onboarding_started" | "onboarding_step" | "onboarding_completed" | "paywall_shown" | "paywall_dismissed" | "start_checkout" | "trial_started" | "subscribe" | "purchase" | "renewal" | "cancellation" | "expiration" | "refund" | "add_to_cart" | "level_complete" | "tutorial_complete" | "content_view";
42
+ /**
43
+ * Event name as accepted on the wire: a canonical name (autocompleted) OR any
44
+ * custom string the integrating app chooses. `(string & {})` keeps the union
45
+ * literals in IntelliSense while still allowing arbitrary strings.
46
+ */
47
+ export type AttributionEventName = CanonicalEventName | (string & {});
48
+ /**
49
+ * Identifiers and PII for the acting user. All PII fields (`email`, `phone`)
50
+ * are sent as raw values through the pipeline and hashed at the adapter layer
51
+ * right before being shipped to Meta CAPI / TikTok Events API — never logged
52
+ * raw, never persisted raw.
53
+ */
54
+ export interface AttributionUser {
55
+ /** Stable internal user id supplied by the integrating app. */
56
+ userId?: string;
57
+ /** iOS Identifier for Vendor (always available, no ATT required). */
58
+ idfv?: string;
59
+ /** iOS Advertising Identifier (only available when ATT === 'authorized'). */
60
+ idfa?: string;
61
+ /** Google Advertising ID (Android only). */
62
+ gaid?: string;
63
+ /** PII — hashed at adapter boundary (Meta `em`, TikTok `email`). */
64
+ email?: string;
65
+ /** PII — hashed at adapter boundary (Meta `ph`, TikTok `phone_number`). */
66
+ phone?: string;
67
+ /** ISO 3166-1 alpha-2, lower-case (e.g. "br", "us"). */
68
+ country?: string;
69
+ /** BCP-47 locale (e.g. "pt-BR", "en-US"). */
70
+ locale?: string;
71
+ /** ATT snapshot at event time. */
72
+ attStatus?: AttStatus;
73
+ /**
74
+ * Client IP as observed by the server at request time. Populated SERVER-side
75
+ * from the request socket — never trust a wire value. Boosts Event Match
76
+ * Quality (Meta `client_ip_address`, TikTok `ip`). Never logged/persisted raw
77
+ * (PII redaction strips it before Mongo).
78
+ */
79
+ ipAddress?: string;
80
+ /** User agent from the same request as `ipAddress`. Server-side capture only. */
81
+ userAgent?: string;
82
+ /** Meta browser cookie (`_fbp`). Sent to Meta CAPI as `user_data.fbp`. */
83
+ fbp?: string;
84
+ /** Meta click cookie (`_fbc`). Highest-impact field for paid Meta traffic. */
85
+ fbc?: string;
86
+ /** TikTok click id (`ttclid`). Deterministic match key for paid TikTok. */
87
+ ttclid?: string;
88
+ /** TikTok pixel cookie (`_ttp`). */
89
+ ttp?: string;
90
+ }
91
+ /**
92
+ * Monetary information for revenue-bearing events. The server uses `netValue`
93
+ * to teach the ad algorithm; do not use `grossValue` for optimization (it
94
+ * inflates LTV by the store's commission).
95
+ */
96
+ export interface RevenueData {
97
+ /** What the user paid, before any commission. Used for accounting / BI. */
98
+ grossValue: number;
99
+ /** What the app actually receives after store fee. Used for optimization. */
100
+ netValue: number;
101
+ /** ISO 4217, upper-case. */
102
+ currency: Currency;
103
+ /** Where the money came from. Determines the default fee rate. */
104
+ store: StorePlatform;
105
+ /** Store-issued transaction id (App Store original_transaction_id, etc.). */
106
+ transactionId?: string;
107
+ }
108
+ /**
109
+ * Optional marketing attribution context. Populated by the server when a
110
+ * webhook/event arrives (joining `userId` → first-touch campaign captured at
111
+ * install) or by deep-link adapters on the client.
112
+ */
113
+ export interface AttributionContext {
114
+ campaignId?: string;
115
+ adsetId?: string;
116
+ adId?: string;
117
+ creativeId?: string;
118
+ source?: "meta" | "tiktok" | "google" | "organic" | "referral" | "unknown";
119
+ /** Stable token returned by `POST /v1/click`. Joins user → click. */
120
+ clickToken?: string;
121
+ utmSource?: string;
122
+ utmMedium?: string;
123
+ utmCampaign?: string;
124
+ utmContent?: string;
125
+ utmTerm?: string;
126
+ /** 1.0 = deterministic (Universal Link); <1.0 = probabilistic fingerprint. */
127
+ matchConfidence?: number;
128
+ matchMethod?: "universal_link" | "fingerprint" | "manual" | "none";
129
+ }
130
+ /**
131
+ * The single canonical event shape that flows through every layer. Adapters at
132
+ * the edge translate it to vendor formats; the Mongo collection stores it as-is
133
+ * for reporting / dashboard.
134
+ */
135
+ export interface AttributionEvent {
136
+ /**
137
+ * Tenant scope. Every read/write on the server is filtered by this, and the
138
+ * per-app provider credentials are resolved from it.
139
+ */
140
+ appId: string;
141
+ /**
142
+ * Deterministic event id (UUID v5) — see `eventId.ts`. Identical across the
143
+ * in-app SDK, Meta CAPI and TikTok Events API for a given underlying action.
144
+ * Powers the dedup that lets us double-send for resilience without
145
+ * double-counting.
146
+ */
147
+ eventId: string;
148
+ name: AttributionEventName;
149
+ /** Unix epoch in **seconds** (Meta CAPI / TikTok EAPI convention). */
150
+ timestamp: number;
151
+ platform: Platform;
152
+ user: AttributionUser;
153
+ /** Present on monetization events. */
154
+ revenue?: RevenueData;
155
+ /** Store product id (e.g. "pro_weekly"). */
156
+ productId?: string;
157
+ /** Marketing attribution joined on the server side. */
158
+ attribution?: AttributionContext;
159
+ /** Free-form extras flattened into vendor `custom_data` / `properties`. */
160
+ properties?: Record<string, string | number | boolean | null>;
161
+ }
162
+ /**
163
+ * Subset of `AttributionEvent` callers supply when emitting an event; the
164
+ * orchestrator fills in `eventId`, `timestamp`, and `platform`. `appId` is
165
+ * resolved from the authenticated API key on the server, so the SDK does not
166
+ * need to send it explicitly (though it may).
167
+ */
168
+ export type AttributionEventInput = Omit<AttributionEvent, "eventId" | "timestamp" | "platform" | "appId"> & {
169
+ appId?: string;
170
+ eventId?: string;
171
+ timestamp?: number;
172
+ platform?: Platform;
173
+ };
174
+ /** Allowed value types inside `properties` (JSON scalars only). */
175
+ export type ProductPropertyValue = string | number | boolean | null;
176
+ /** Lightweight device/app context captured automatically by the SDK. */
177
+ export interface ProductEventContext {
178
+ appVersion?: string;
179
+ osVersion?: string;
180
+ deviceModel?: string;
181
+ /** BCP-47 locale (e.g. "pt-BR"). */
182
+ locale?: string;
183
+ /** ISO 3166-1 alpha-2 (e.g. "br"). */
184
+ country?: string;
185
+ /** SDK name/version that produced the event (e.g. "trackstall-js/0.1"). */
186
+ sdk?: string;
187
+ }
188
+ /**
189
+ * The canonical product-analytics event. One row per occurrence in the
190
+ * `product_events` collection; `(appId, eventId)` is the dedup key.
191
+ */
192
+ export interface ProductEvent {
193
+ /** Tenant scope — stamped server-side from the authenticated API key. */
194
+ appId: string;
195
+ /** Unique id for dedup across SDK retries. UUID, client-generated. */
196
+ eventId: string;
197
+ /** Free-form event name chosen by the app (e.g. "screen_view"). */
198
+ name: string;
199
+ /** Unix epoch in **seconds** (same convention as attribution events). */
200
+ timestamp: number;
201
+ platform: Platform;
202
+ /**
203
+ * Who did it: the app's stable `userId` when identified, otherwise the
204
+ * SDK-generated anonymous id. Powers unique-user counts.
205
+ */
206
+ distinctId: string;
207
+ /** Set when the user is identified (mirrors `distinctId` in that case). */
208
+ userId?: string;
209
+ /** Device-generated anonymous id (kept after identify for stitching). */
210
+ anonymousId?: string;
211
+ /** Optional client-managed session id for session analytics. */
212
+ sessionId?: string;
213
+ /** Free-form event payload. JSON scalars only; no PII. */
214
+ properties?: Record<string, ProductPropertyValue>;
215
+ context?: ProductEventContext;
216
+ }
217
+ /**
218
+ * Wire input accepted by `POST /v1/track`. The server stamps `appId` from the
219
+ * API key and fills `eventId`/`timestamp` when omitted. `distinctId` may be
220
+ * derived from `userId`/`anonymousId` when not set explicitly.
221
+ */
222
+ export type ProductEventInput = Omit<ProductEvent, "appId" | "eventId" | "timestamp" | "distinctId"> & {
223
+ appId?: string;
224
+ eventId?: string;
225
+ timestamp?: number;
226
+ distinctId?: string;
227
+ };
228
+ export interface DeterministicEventIdInput {
229
+ /**
230
+ * Tenant scope. Included in the dedup key so the same logical action in two
231
+ * different apps never collides on a shared `userId`/`transactionId`.
232
+ */
233
+ appId: string;
234
+ /** Stable user identifier supplied by the integrating app. */
235
+ userId: string;
236
+ eventName: AttributionEventName;
237
+ /**
238
+ * Strongest dedup key when present — App Store `original_transaction_id`,
239
+ * Play Store `purchaseToken`, Stripe `payment_intent`, etc. For non-purchase
240
+ * events, prefer a stable client-generated id (persisted) so offline retries
241
+ * collapse to the same `eventId`.
242
+ */
243
+ transactionId?: string;
244
+ /**
245
+ * Unix epoch in seconds. Including it scopes dedup to a single "occurrence"
246
+ * of an otherwise idempotent action (e.g. two renewals on the same
247
+ * transactionId still produce different ids). Omit for interchangeable
248
+ * actions (e.g. the first `install` for a user).
249
+ */
250
+ timestamp?: number;
251
+ }
252
+ /**
253
+ * Deterministic event id used as the dedup key across every channel (in-app
254
+ * SDK → ad platform pixel/SDK; backend webhook → Meta CAPI / TikTok EAPI).
255
+ * Same inputs always produce the same UUID.
256
+ */
257
+ export declare function deterministicEventId(input: DeterministicEventIdInput): string;
258
+ /**
259
+ * Non-deterministic event id for one-off events with no stable dedup key.
260
+ * Avoid for monetization.
261
+ */
262
+ export declare function randomEventId(): string;
263
+ export interface BuildRevenueInput {
264
+ /** Gross amount the user paid (default) or net amount if `netOfStoreFee`. */
265
+ value: number;
266
+ /** ISO 4217 — will be upper-cased. */
267
+ currency: string;
268
+ store: StorePlatform;
269
+ transactionId?: string;
270
+ /** Custom fee rate (0..1). Overrides `STORE_FEE_DEFAULTS[store]`. */
271
+ feeRateOverride?: number;
272
+ /** `true` if `value` is already net of the store commission. */
273
+ netOfStoreFee?: boolean;
274
+ }
275
+ /**
276
+ * Build a normalized `RevenueData` block.
277
+ *
278
+ * Why both `grossValue` and `netValue`:
279
+ * - `grossValue` → revenue reports / BI ("the user paid R$299").
280
+ * - `netValue` → the `value` sent to Meta CAPI / TikTok EAPI, so the
281
+ * optimization algorithm learns from money actually received, not the
282
+ * store's cut. The single most impactful tweak after dedup.
283
+ */
284
+ export declare function buildRevenue(input: BuildRevenueInput): RevenueData;
285
+ export interface AdapterDeliveryResult {
286
+ ok: boolean;
287
+ /** Short machine code (`unmapped`, `sdk_threw`, `sdk_not_initialized`, ...). */
288
+ code?: string;
289
+ message?: string;
290
+ /** Whether the host could retry. Native adapters are fire-and-forget today. */
291
+ retriable?: boolean;
292
+ }
293
+ export interface NativeAdapter {
294
+ readonly name: string;
295
+ /** Boot the underlying native SDK. Called once from `client.initialize()`. */
296
+ initialize(): Promise<void>;
297
+ /** Propagate (or clear) user identity to the SDK. */
298
+ setUser(identity: UserIdentity | null): void;
299
+ /** Fire one canonical event into the SDK. */
300
+ deliver(event: AttributionEvent): Promise<AdapterDeliveryResult>;
301
+ /** Flush the SDK's internal buffer, if it has one. */
302
+ flush?(): Promise<void>;
303
+ /** Update ATT/advertiser-tracking consent (Meta). */
304
+ setAdvertiserTracking?(enabled: boolean): Promise<void>;
305
+ }
306
+ export interface MetaAdapterOptions {
307
+ /** Meta App ID (developers.facebook.com). */
308
+ appId: string;
309
+ /** Meta Client Token (Settings → Advanced → Client Token). */
310
+ clientToken: string;
311
+ /**
312
+ * Send `AdvertiserTrackingEnabled` based on ATT status. Default true —
313
+ * required on iOS 14.5+ to reach Meta's algorithms (not just SKAdNetwork).
314
+ */
315
+ honorAtt?: boolean;
316
+ /** Override the adapter name (default `'meta'`). */
317
+ name?: string;
318
+ /** Injectable Meta SDK module — for tests. Default: lazy `require`. */
319
+ sdkLoader?: () => MetaSdkModule;
320
+ }
321
+ /** Minimal slice of `react-native-fbsdk-next` the adapter uses. */
322
+ export interface MetaSdkModule {
323
+ Settings: {
324
+ setAppID(appId: string): void;
325
+ setClientToken(token: string): void;
326
+ initializeSDK(): void;
327
+ setAdvertiserTrackingEnabled?(enabled: boolean): Promise<void> | void;
328
+ setAdvertiserIDCollectionEnabled?(enabled: boolean): Promise<void> | void;
329
+ setAutoLogAppEventsEnabled?(enabled: boolean): Promise<void> | void;
330
+ };
331
+ AppEventsLogger: {
332
+ logEvent(eventName: string, parameters?: Record<string, unknown>): void;
333
+ logEvent(eventName: string, valueToSum: number, parameters?: Record<string, unknown>): void;
334
+ logPurchase(amount: number, currency: string, parameters?: Record<string, unknown>): void;
335
+ setUserID(userId: string): void;
336
+ clearUserID(): void;
337
+ setUserData(userData: Record<string, string | null | undefined>): void;
338
+ flush(): void;
339
+ clearUserData?(): void;
340
+ };
341
+ }
342
+ export declare class MetaAdapter implements NativeAdapter {
343
+ private readonly options;
344
+ readonly name: string;
345
+ private sdk;
346
+ private readonly honorAtt;
347
+ constructor(options: MetaAdapterOptions);
348
+ initialize(): Promise<void>;
349
+ setUser(identity: UserIdentity | null): void;
350
+ deliver(event: AttributionEvent): Promise<AdapterDeliveryResult>;
351
+ flush(): Promise<void>;
352
+ setAdvertiserTracking(enabled: boolean): Promise<void>;
353
+ private loadSdk;
354
+ }
355
+ export interface MetaMappedEvent {
356
+ name: string;
357
+ kind: "purchase" | "standard" | "custom";
358
+ amount?: number;
359
+ currency?: string;
360
+ valueToSum?: number;
361
+ parameters: Record<string, unknown>;
362
+ }
363
+ /** Map TrackStall canonical names → Meta `AppEvents`. `null` = skip silently. */
364
+ export declare function mapToMetaEvent(event: AttributionEvent): MetaMappedEvent | null;
365
+ export interface TikTokAdapterOptions {
366
+ /** App bundle id (iOS) / package name (Android). */
367
+ appId: string;
368
+ /** TikTok App ID (Business Center → App → Settings). */
369
+ tiktokAppId: string;
370
+ /** Access token the TikTok SDK needs at `initialize()`. */
371
+ accessToken: string;
372
+ /** Default false. True while validating in TikTok Events Manager. */
373
+ debugMode?: boolean;
374
+ /** Override adapter name (default `'tiktok'`). */
375
+ name?: string;
376
+ /** Injectable SDK module — for tests. */
377
+ sdkLoader?: () => TikTokSdkModule;
378
+ }
379
+ /** Minimal surface of `@layers/expo-tiktok-business`. */
380
+ export interface TikTokSdkModule {
381
+ default: {
382
+ initialize(appId: string, tiktokAppId: string, options: {
383
+ accessToken: string;
384
+ debugMode?: boolean;
385
+ autoTrackAppLifecycle?: boolean;
386
+ autoTrackRouteChanges?: boolean;
387
+ }): Promise<boolean>;
388
+ trackEvent(name: string, params?: Record<string, unknown>): Promise<boolean>;
389
+ trackCompletePurchase(value: number, currency: string, contents: Array<{
390
+ content_id: string;
391
+ content_type?: string;
392
+ content_name?: string;
393
+ quantity?: number;
394
+ price?: number;
395
+ }>, additionalParams?: Record<string, unknown>): Promise<boolean>;
396
+ };
397
+ }
398
+ export declare class TikTokAdapter implements NativeAdapter {
399
+ private readonly options;
400
+ readonly name: string;
401
+ private sdk;
402
+ private currentUserId;
403
+ constructor(options: TikTokAdapterOptions);
404
+ initialize(): Promise<void>;
405
+ setUser(identity: UserIdentity | null): void;
406
+ deliver(event: AttributionEvent): Promise<AdapterDeliveryResult>;
407
+ private loadSdk;
408
+ }
409
+ export interface TikTokMappedEvent {
410
+ name: string;
411
+ kind: "purchase" | "standard" | "custom";
412
+ value?: number;
413
+ currency?: string;
414
+ contents?: Array<{
415
+ content_id: string;
416
+ content_type?: string;
417
+ content_name?: string;
418
+ quantity?: number;
419
+ price?: number;
420
+ }>;
421
+ params: Record<string, unknown>;
422
+ }
423
+ /** Map TrackStall canonical names → TikTok standard events. `null` = skip. */
424
+ export declare function mapToTikTokEvent(event: AttributionEvent, userId: string | null): TikTokMappedEvent | null;
425
+ /**
426
+ * Minimal slice of the tracking-transparency packages. Either pair works:
427
+ * Expo (`requestTrackingPermissionsAsync`) or bare RN
428
+ * (`requestTrackingPermission`).
429
+ */
430
+ export interface AttSdkModule {
431
+ requestTrackingPermissionsAsync?(): Promise<{
432
+ status: string;
433
+ }>;
434
+ getTrackingPermissionsAsync?(): Promise<{
435
+ status: string;
436
+ }>;
437
+ /** expo-tracking-transparency: returns the IDFA once tracking is authorized. */
438
+ getAdvertisingId?(): string | null;
439
+ requestTrackingPermission?(): Promise<string>;
440
+ getTrackingStatus?(): Promise<string>;
441
+ }
442
+ export declare function loadAttModule(injected?: AttSdkModule): AttSdkModule | null;
443
+ /** Show the native ATT prompt and return the normalized status. */
444
+ export declare function requestAttStatus(mod: AttSdkModule): Promise<AttStatus>;
445
+ /**
446
+ * Read the device advertising id (IDFA) when tracking is authorized. Returns
447
+ * `undefined` when the module can't provide it or tracking isn't authorized
448
+ * (the vendor returns a zeroed UUID in that case, which we filter out).
449
+ */
450
+ export declare function readAdvertisingId(mod: AttSdkModule): string | undefined;
451
+ /** Read the current ATT status without prompting. */
452
+ export declare function getAttStatus(mod: AttSdkModule): Promise<AttStatus>;
453
+ /** Map vendor status strings onto TrackStall's canonical `AttStatus`. */
454
+ export declare function normalizeAttStatus(raw: string | undefined): AttStatus;
455
+ /**
456
+ * Exponential-backoff retry calculator for backend deliveries. Pure functions —
457
+ * no timers, no `Date.now()`. The queue stores `nextRetryAt` (epoch ms) and the
458
+ * flush loop pulls ready rows on each tick.
459
+ *
460
+ * delay(attempt) = clamp(initialDelay * 2^(attempt-1), 0, maxDelay) ± jitter
461
+ */
462
+ export interface RetryConfig {
463
+ /** Max attempts (including the first). Default 6. */
464
+ maxAttempts?: number;
465
+ /** First retry delay in ms. Default 1000ms. */
466
+ initialDelayMs?: number;
467
+ /** Cap on individual delays. Default 5 minutes. */
468
+ maxDelayMs?: number;
469
+ /** Jitter factor [0, 1). Default 0.2 → ±20%. */
470
+ jitter?: number;
471
+ }
472
+ export declare const DEFAULT_RETRY: Required<RetryConfig>;
473
+ export declare function resolveRetryConfig(input?: RetryConfig): Required<RetryConfig>;
474
+ export declare function computeRetryDelayMs(attempt: number, config: Required<RetryConfig>, random?: () => number): number;
475
+ export declare function shouldRetry(attemptsSoFar: number, retriable: boolean | undefined, config: Required<RetryConfig>): boolean;
476
+ /**
477
+ * Minimal async key-value contract the SDK persists against. Deliberately a
478
+ * subset of `@react-native-async-storage/async-storage` so that package drops
479
+ * in directly; `localStorage` and an in-memory fallback are adapted to it too.
480
+ */
481
+ export interface KeyValueStorage {
482
+ getItem(key: string): Promise<string | null>;
483
+ setItem(key: string, value: string): Promise<void>;
484
+ removeItem(key: string): Promise<void>;
485
+ }
486
+ /**
487
+ * Client-safe runtime config served by `GET /v1/config`. Mirrors the server's
488
+ * `ClientRuntimeConfig`. Holds ONLY values the device may hold (App IDs, client
489
+ * token, device access token) — never the CAPI/EAPI server tokens.
490
+ */
491
+ export interface RemoteConfig {
492
+ appId: string;
493
+ sandbox: boolean;
494
+ meta: {
495
+ appId: string;
496
+ clientToken: string;
497
+ } | null;
498
+ tiktok: {
499
+ appId: string;
500
+ ttAppId: string;
501
+ accessToken: string;
502
+ } | null;
503
+ }
504
+ export interface ClientLogger {
505
+ debug(message: string, context?: Record<string, unknown>): void;
506
+ info(message: string, context?: Record<string, unknown>): void;
507
+ warn(message: string, context?: Record<string, unknown>): void;
508
+ error(message: string, error?: unknown, context?: Record<string, unknown>): void;
509
+ }
510
+ export interface DeviceContext {
511
+ bundleId?: string;
512
+ appVersion?: string;
513
+ /** Two-letter country (e.g. `BR`). Improves match quality. */
514
+ country?: string;
515
+ /** IETF language tag (e.g. `pt-BR`). */
516
+ locale?: string;
517
+ timezone?: string;
518
+ /** Device advertising identifiers, when available + permitted. */
519
+ idfa?: string;
520
+ idfv?: string;
521
+ gaid?: string;
522
+ attStatus?: AttStatus;
523
+ /**
524
+ * Probabilistic-match fingerprint signals. Feeding these lifts install
525
+ * matching from the weak IP+locale tier (0.60–0.85) to the strong
526
+ * device-fingerprint tiers (0.82–0.90). Without them, click→install
527
+ * matching on iOS (no deterministic id from Safari) is effectively blind.
528
+ * Populate from React Native `Dimensions`/`Platform`/`expo-device`.
529
+ */
530
+ screenW?: number;
531
+ screenH?: number;
532
+ pixelRatio?: number;
533
+ osVersion?: string;
534
+ deviceModel?: string;
535
+ }
536
+ export interface UserIdentity {
537
+ /** Stable internal id — matches the server's `user.userId`. Required. */
538
+ userId: string;
539
+ email?: string;
540
+ phone?: string;
541
+ country?: string;
542
+ locale?: string;
543
+ }
544
+ export interface TrackEventInput {
545
+ name: AttributionEventName;
546
+ revenue?: RevenueData;
547
+ productId?: string;
548
+ /** Free-form extras → server `properties`. No PII here. */
549
+ properties?: Record<string, string | number | boolean | null>;
550
+ /** Override the deterministic event id. */
551
+ eventId?: string;
552
+ /** Override event timestamp (epoch ms). Defaults to now. */
553
+ timestampMs?: number;
554
+ /** Per-event user override (log an event for a different user). */
555
+ user?: UserIdentity;
556
+ /** Mark this event as sandbox/test traffic. */
557
+ sandbox?: boolean;
558
+ }
559
+ /**
560
+ * Optional knobs for `logEvent()` — the product-analytics lane (Mixpanel
561
+ * style). Most apps never need this: `ts.logEvent('screen_view', { screen:
562
+ * 'home' })` is the whole integration.
563
+ */
564
+ export interface LogEventOptions {
565
+ /** Override the random event id (dedup key on the server). */
566
+ eventId?: string;
567
+ /** Override event timestamp (epoch ms). Defaults to now. */
568
+ timestampMs?: number;
569
+ /** Client-managed session id for session analytics. */
570
+ sessionId?: string;
571
+ }
572
+ /** Free-form product-analytics payload. JSON scalars only; no PII. */
573
+ export type LogEventProperties = Record<string, ProductPropertyValue>;
574
+ /** Click capture input — fields mirror `POST /v1/click`. */
575
+ export interface RecordClickInput {
576
+ fbclid?: string;
577
+ ttclid?: string;
578
+ gclid?: string;
579
+ fbp?: string;
580
+ fbc?: string;
581
+ ttp?: string;
582
+ utmSource?: string;
583
+ utmMedium?: string;
584
+ utmCampaign?: string;
585
+ utmContent?: string;
586
+ utmTerm?: string;
587
+ campaignId?: string;
588
+ adsetId?: string;
589
+ adId?: string;
590
+ creativeId?: string;
591
+ landingUrl?: string;
592
+ referrer?: string;
593
+ locale?: string;
594
+ timezone?: string;
595
+ country?: string;
596
+ screenW?: number;
597
+ screenH?: number;
598
+ pixelRatio?: number;
599
+ osVersion?: string;
600
+ deviceModel?: string;
601
+ }
602
+ export interface RecordClickResult {
603
+ clickToken: string;
604
+ expiresAt: string;
605
+ }
606
+ /** Install-match input — fields mirror `POST /v1/match`. */
607
+ export interface MatchInstallInput {
608
+ userId?: string;
609
+ clickToken?: string;
610
+ locale?: string;
611
+ timezone?: string;
612
+ screenW?: number;
613
+ screenH?: number;
614
+ pixelRatio?: number;
615
+ osVersion?: string;
616
+ deviceModel?: string;
617
+ }
618
+ export interface MatchInstallResult {
619
+ matched: boolean;
620
+ matchMethod: string;
621
+ matchConfidence: number;
622
+ source: string | null;
623
+ campaignId: string | null;
624
+ }
625
+ /**
626
+ * Install-referrer input (Android) — fields mirror `POST /v1/referrer`. Pass the
627
+ * raw referrer string read from the Play Install Referrer API
628
+ * (`com.android.installreferrer` / `react-native-play-install-referrer`).
629
+ */
630
+ export interface InstallReferrerInput {
631
+ userId?: string;
632
+ referrer: string;
633
+ locale?: string;
634
+ timezone?: string;
635
+ country?: string;
636
+ screenW?: number;
637
+ screenH?: number;
638
+ pixelRatio?: number;
639
+ osVersion?: string;
640
+ deviceModel?: string;
641
+ }
642
+ /** Mirrors `MatchInstallResult` — the referrer match is deterministic (conf 1.0). */
643
+ export type InstallReferrerResult = MatchInstallResult;
644
+ export interface TrackStallConfig {
645
+ /** Tenant id assigned by TrackStall. */
646
+ appId: string;
647
+ /** Public API key (`tsk_pub_...`). Authenticates the SDK. */
648
+ publicKey: string;
649
+ /** Base URL of the TrackStall API (no trailing slash), e.g. `https://api.trackstall.io`. */
650
+ endpoint: string;
651
+ /** Runtime platform. Defaults to `'ios'` if unset and not inferable. */
652
+ platform?: Platform;
653
+ device?: DeviceContext;
654
+ storage?: KeyValueStorage;
655
+ logger?: ClientLogger;
656
+ /** Master kill-switch. When `false`, every call silently no-ops. */
657
+ enabled?: boolean;
658
+ /** Inject a fetch implementation (default: global `fetch`). */
659
+ fetchImpl?: typeof fetch;
660
+ /** Per-request timeout in ms. Default 8000. */
661
+ requestTimeoutMs?: number;
662
+ /** Offline buffer capacity (FIFO drop when full). Default 500. */
663
+ bufferCapacity?: number;
664
+ /** Flush batch size for `/v1/events`. Default 20. */
665
+ batchSize?: number;
666
+ retry?: RetryConfig;
667
+ now?: () => number;
668
+ /**
669
+ * Disable best-effort device auto-collection on `initialize()` (screen/OS/model
670
+ * fingerprint signals from React Native / `expo-device`). Host-supplied
671
+ * `config.device` values always take precedence regardless. Default false.
672
+ */
673
+ disableAutoDeviceContext?: boolean;
674
+ /**
675
+ * Inject the native SDK modules. By default each adapter lazy-`require()`s
676
+ * its package (`react-native-fbsdk-next`, `@layers/expo-tiktok-business`,
677
+ * `expo-tracking-transparency` / `react-native-tracking-transparency`).
678
+ * Pass these to inject explicitly (tests, or non-standard resolution).
679
+ */
680
+ native?: {
681
+ meta?: MetaSdkModule;
682
+ tiktok?: TikTokSdkModule;
683
+ att?: AttSdkModule;
684
+ };
685
+ /**
686
+ * Disable the `GET /v1/config` fetch and native dual-send entirely — only the
687
+ * backend (HTTP → CAPI/EAPI) lane runs. Use on web/server. Default false.
688
+ */
689
+ disableNativeDualSend?: boolean;
690
+ /**
691
+ * Override the remote config (skip the `/v1/config` fetch). Tests / advanced
692
+ * setups that already hold the client-safe config.
693
+ */
694
+ remoteConfig?: RemoteConfig;
695
+ }
696
+ export interface FlushResult {
697
+ attempted: number;
698
+ delivered: number;
699
+ retryScheduled: number;
700
+ dropped: number;
701
+ }
702
+ export interface ClientDiagnostics {
703
+ initialized: boolean;
704
+ enabled: boolean;
705
+ appId: string;
706
+ userId: string | null;
707
+ anonymousId: string | null;
708
+ queueSize: number;
709
+ analyticsQueueSize: number;
710
+ }
711
+ export interface TrackStallClient {
712
+ initialize(): Promise<void>;
713
+ setUser(identity: UserIdentity | null): void;
714
+ recordClick(input: RecordClickInput): Promise<RecordClickResult>;
715
+ matchInstall(input?: MatchInstallInput): Promise<MatchInstallResult>;
716
+ /**
717
+ * ANDROID deterministic attribution: report the Google Play Install Referrer
718
+ * string (read once on first launch via the Play Install Referrer API). The
719
+ * backend parses the ad-network click id out of it and enriches future events
720
+ * at confidence 1.0 — the Android equivalent of a Universal Link `clickToken`.
721
+ */
722
+ reportInstallReferrer(input: InstallReferrerInput): Promise<InstallReferrerResult>;
723
+ /**
724
+ * ATTRIBUTION lane: enqueue a marketing event for durable delivery to the
725
+ * ad networks (Meta CAPI / TikTok EAPI). Returns the canonical `eventId`.
726
+ */
727
+ track(input: TrackEventInput): Promise<string>;
728
+ /**
729
+ * PRODUCT ANALYTICS lane (Mixpanel-style): log an in-app event to the
730
+ * TrackStall dashboard only — never forwarded to ad networks.
731
+ *
732
+ * ts.logEvent('screen_view', { screen: 'home' });
733
+ *
734
+ * Identity is automatic: the current user's `userId` when set, otherwise a
735
+ * persistent device-generated anonymous id. Returns the `eventId`.
736
+ */
737
+ logEvent(name: string, properties?: LogEventProperties, options?: LogEventOptions): Promise<string>;
738
+ /**
739
+ * Show the native ATT prompt (iOS) and report the resulting status to the
740
+ * TrackStall backend automatically — no extra code needed in the app.
741
+ * Resolves the tracking-transparency module lazily (`expo-tracking-transparency`
742
+ * or `react-native-tracking-transparency`); returns `'unavailable'` when the
743
+ * platform isn't iOS or no module is installed.
744
+ */
745
+ requestAttPermission(): Promise<AttStatus>;
746
+ /**
747
+ * Report an ATT status obtained by the app itself (when the host app manages
748
+ * the prompt). Also stamps `attStatus` on subsequent attribution events.
749
+ */
750
+ setAttStatus(status: AttStatus): Promise<void>;
751
+ flush(): Promise<FlushResult>;
752
+ getDiagnostics(): ClientDiagnostics;
753
+ }
754
+ export declare function createTrackStallClient(config: TrackStallConfig): TrackStallClient;
755
+ /**
756
+ * HTTP transport for the TrackStall API. Adds the public-key auth header,
757
+ * enforces a per-request timeout, and classifies the response so the queue
758
+ * knows whether to retry (5xx / 408 / 429 / network) or drop (4xx schema/auth).
759
+ */
760
+ export interface TransportResponse {
761
+ ok: boolean;
762
+ status: number;
763
+ body: unknown;
764
+ retriable: boolean;
765
+ errorMessage?: string;
766
+ }
767
+ export interface TransportOptions {
768
+ endpoint: string;
769
+ publicKey: string;
770
+ fetchImpl?: typeof fetch;
771
+ timeoutMs?: number;
772
+ }
773
+ export declare class Transport {
774
+ private readonly endpoint;
775
+ private readonly publicKey;
776
+ private readonly fetchImpl;
777
+ private readonly timeoutMs;
778
+ constructor(options: TransportOptions);
779
+ get(path: string): Promise<TransportResponse>;
780
+ post(path: string, body: unknown): Promise<TransportResponse>;
781
+ private send;
782
+ }
783
+ export declare function isRetriableStatus(status: number): boolean;
784
+ export interface QueuedEvent {
785
+ /** Local queue id (not the attribution eventId). */
786
+ id: string;
787
+ /** Server dedup id — also used to match per-event results on flush. */
788
+ eventId: string;
789
+ /** The wire body posted inside `{ events: [...] }`. */
790
+ body: Record<string, unknown>;
791
+ attempts: number;
792
+ nextRetryAt: number;
793
+ createdAt: number;
794
+ }
795
+ export interface EventQueueOptions {
796
+ storage: KeyValueStorage;
797
+ logger: ClientLogger;
798
+ capacity?: number;
799
+ storageKey?: string;
800
+ now?: () => number;
801
+ }
802
+ export declare class EventQueue {
803
+ private readonly storage;
804
+ private readonly logger;
805
+ private readonly capacity;
806
+ private readonly storageKey;
807
+ private readonly now;
808
+ private entries;
809
+ private hydrated;
810
+ private counter;
811
+ private chain;
812
+ constructor(options: EventQueueOptions);
813
+ hydrate(): Promise<void>;
814
+ size(): number;
815
+ enqueue(eventId: string, body: Record<string, unknown>): Promise<void>;
816
+ /** Snapshot of entries eligible to send now (does not mutate). */
817
+ pullReady(limit: number): QueuedEvent[];
818
+ markDelivered(ids: string[]): Promise<void>;
819
+ markFailed(ids: string[], nextRetryAt: number): Promise<void>;
820
+ clear(): Promise<void>;
821
+ private persist;
822
+ private serialize;
823
+ }
824
+ /** In-memory storage. Default when no persistence is provided — the offline
825
+ * buffer is then lost across app launches (fine for web sessions / tests). */
826
+ export declare class MemoryStorage implements KeyValueStorage {
827
+ private readonly map;
828
+ getItem(key: string): Promise<string | null>;
829
+ setItem(key: string, value: string): Promise<void>;
830
+ removeItem(key: string): Promise<void>;
831
+ }
832
+ /**
833
+ * Adapts a synchronous Web Storage (`localStorage`) to the async contract.
834
+ * Returns a `MemoryStorage` when no Web Storage is available (RN, SSR, Node).
835
+ */
836
+ export declare function defaultStorage(): KeyValueStorage;
837
+ /**
838
+ * Collect whatever device signals are available without any user permission.
839
+ * Returns a partial `DeviceContext`; missing fields are simply omitted.
840
+ */
841
+ export declare function collectDeviceContext(): Partial<DeviceContext>;
842
+
843
+ export {};