@bytem/bytem-tracker-app 0.0.6 → 0.0.7

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.
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  This is the official Bytem Tracker SDK for React Native applications.
4
4
 
5
+ ## Documentation
6
+
7
+ - [Usage Guide](docs/USAGE.md): Detailed instructions on initialization, event tracking, and API usage.
8
+
5
9
  ## Installation
6
10
 
7
11
  Install the package and its peer dependencies using Yarn:
@@ -60,95 +64,3 @@ Notes:
60
64
  - These scripts bump package.json version and publish to npm.
61
65
  - `prepublishOnly` builds the package before publishing.
62
66
  - Configure auth via user-level `~/.npmrc`. Do not commit tokens in the repo.
63
- ## Usage
64
-
65
- ### Initialization
66
- Initialize the tracker in your app entry point (e.g., `App.tsx` or `index.js`). Note that `init` is asynchronous.
67
-
68
- ```typescript
69
- import React, { useEffect } from 'react';
70
- import BytemTracker from '@bytem/bytem-tracker-app';
71
-
72
- const App = () => {
73
- useEffect(() => {
74
- const initTracker = async () => {
75
- await BytemTracker.init({
76
- appId: 'your-app-id',
77
- endpoint: 'https://api.bytem.com/track',
78
- debug: __DEV__, // Enable debug logs in development
79
- });
80
- };
81
-
82
- initTracker();
83
- }, []);
84
-
85
- return <YourApp />;
86
- };
87
- ```
88
-
89
- ### Tracking Events
90
-
91
- #### Identify User
92
- Track user identification when a user logs in or updates their profile.
93
-
94
- ```typescript
95
- // Signature: trackUser(userId: string, traits?: object)
96
- BytemTracker.trackUser('user_12345', {
97
- email: 'user@example.com',
98
- age: 25,
99
- membership: 'gold'
100
- });
101
- ```
102
-
103
- #### View Product
104
- Track when a user views a product details page.
105
-
106
- ```typescript
107
- BytemTracker.trackViewProduct({
108
- productId: 'prod_001',
109
- name: 'Awesome Gadget',
110
- price: 99.99,
111
- currency: 'USD',
112
- category: 'Electronics'
113
- });
114
- ```
115
-
116
- #### Checkout Order
117
- Track when a user initiates checkout.
118
-
119
- ```typescript
120
- BytemTracker.trackCheckOutOrder({
121
- orderId: 'order_abc123',
122
- total: 150.00,
123
- currency: 'USD',
124
- products: [
125
- { productId: 'prod_001', price: 99.99, quantity: 1 },
126
- { productId: 'prod_002', price: 50.01, quantity: 1 }
127
- ]
128
- });
129
- ```
130
-
131
- #### Pay Order
132
- Track when a user completes a payment.
133
-
134
- ```typescript
135
- BytemTracker.trackPayOrder({
136
- orderId: 'order_abc123',
137
- total: 150.00,
138
- currency: 'USD',
139
- products: [
140
- { productId: 'prod_001', price: 99.99, quantity: 1 },
141
- { productId: 'prod_002', price: 50.01, quantity: 1 }
142
- ]
143
- });
144
- ```
145
-
146
- ## Platform Compatibility
147
-
148
- This SDK is designed for **React Native** and supports:
149
- - iOS
150
- - Android
151
-
152
- It relies on:
153
- - `@react-native-async-storage/async-storage`: For persisting session and visitor IDs.
154
- - `react-native-device-info`: For capturing device metrics (model, OS version, etc.).
@@ -1,18 +1,58 @@
1
1
  import { TrackerConfig, Product, Order, UserTraits } from './types';
2
2
  declare class BytemTracker {
3
3
  private static instance;
4
- private config;
5
- private isInitialized;
6
- private visitorId;
4
+ private readonly SDK_NAME;
5
+ private readonly SDK_VERSION;
7
6
  private readonly DEFAULT_ENDPOINT;
7
+ private readonly DEFAULT_API_PATH;
8
+ private appKey;
9
+ private baseUrl;
10
+ private debug;
11
+ private visitorId;
12
+ private deviceIdType;
13
+ private appScheme;
14
+ private apiPath;
15
+ private isInitialized;
16
+ private sessionStarted;
17
+ private useSessionCookie;
18
+ private sessionCookieTimeout;
19
+ private lastBeat;
20
+ private trackTime;
21
+ private storedDuration;
22
+ private lastViewTime;
23
+ private lastViewStoredDuration;
8
24
  private constructor();
9
25
  static getInstance(): BytemTracker;
10
26
  init(config: TrackerConfig): Promise<void>;
11
- private track;
12
- trackViewProduct(product: Product): void;
13
- trackCheckOutOrder(order: Order): void;
14
- trackPayOrder(order: Order): void;
15
- trackUser(userId: string, traits?: UserTraits): void;
27
+ private ensureInitialized;
28
+ beginSession(force?: boolean): Promise<void>;
29
+ sessionDuration(sec: number): Promise<void>;
30
+ endSession(sec?: number, force?: boolean): Promise<void>;
31
+ trackSessions(): Promise<void>;
32
+ startTime(): void;
33
+ stopTime(): void;
34
+ trackEvent(eventKey: string, segmentation?: Record<string, any>, count?: number, sum?: number, duration?: number): Promise<void>;
35
+ trackPageview(current: string, referrer?: string): Promise<void>;
36
+ trackGoodsClick(params: {
37
+ gid?: string;
38
+ skuid?: string;
39
+ url: string;
40
+ source?: string;
41
+ position?: string;
42
+ current?: string;
43
+ referrer?: string;
44
+ }): Promise<void>;
45
+ private reportViewDuration;
46
+ private getRealDeviceId;
47
+ private getMetrics;
48
+ private buildBaseRequestParams;
49
+ private extendSession;
50
+ private sendRequest;
51
+ trackViewProduct(product: Product): Promise<void>;
52
+ trackCheckOutOrder(order: Order): Promise<void>;
53
+ trackPayOrder(order: Order): Promise<void>;
54
+ trackUser(userId: string, traits?: UserTraits): Promise<void>;
55
+ private sendUserDetails;
16
56
  }
17
57
  declare const _default: BytemTracker;
18
58
  export default _default;
@@ -1,50 +1,33 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
- const base_1 = require("./core/base");
37
- const request_1 = require("./core/request");
38
- const Business = __importStar(require("./business"));
39
- const session_1 = require("./core/session");
3
+ const types_1 = require("./types");
40
4
  const storage_1 = require("./core/storage");
41
5
  const device_1 = require("./core/device");
42
6
  class BytemTracker {
43
7
  constructor() {
44
- this.config = null;
45
- this.isInitialized = false;
8
+ // SDK Constants
9
+ this.SDK_NAME = 'react_native_bytem';
10
+ this.SDK_VERSION = '0.0.1'; // Should match package.json
11
+ this.DEFAULT_ENDPOINT = 'https://tracking.server.bytecon.com';
12
+ this.DEFAULT_API_PATH = '/i';
13
+ // Config
14
+ this.appKey = null;
15
+ this.baseUrl = this.DEFAULT_ENDPOINT;
16
+ this.debug = false;
46
17
  this.visitorId = null;
47
- this.DEFAULT_ENDPOINT = 'https://tracking.server.bytecon.com/i';
18
+ this.deviceIdType = 1; // 1: Auto-generated
19
+ this.appScheme = null;
20
+ this.apiPath = this.DEFAULT_API_PATH;
21
+ this.isInitialized = false;
22
+ // Session Management
23
+ this.sessionStarted = false;
24
+ this.useSessionCookie = true;
25
+ this.sessionCookieTimeout = 30; // minutes
26
+ this.lastBeat = null;
27
+ this.trackTime = true;
28
+ this.storedDuration = 0;
29
+ this.lastViewTime = 0;
30
+ this.lastViewStoredDuration = 0;
48
31
  }
49
32
  static getInstance() {
50
33
  if (!BytemTracker.instance) {
@@ -57,79 +40,435 @@ class BytemTracker {
57
40
  console.warn('[BytemTracker] Already initialized');
58
41
  return;
59
42
  }
60
- this.config = config;
61
- this.isInitialized = true;
43
+ this.appKey = config.appId;
44
+ if (config.endpoint) {
45
+ this.baseUrl = config.endpoint;
46
+ }
47
+ this.debug = !!config.debug;
48
+ this.appScheme = config.appScheme || null;
49
+ if (config.path) {
50
+ this.apiPath = config.path.startsWith('/') ? config.path : '/' + config.path;
51
+ }
62
52
  // Initialize Visitor ID
63
53
  if (config.visitorId) {
64
54
  this.visitorId = config.visitorId;
65
55
  await (0, storage_1.setItem)(storage_1.StorageKeys.VISITOR_ID, this.visitorId);
56
+ if (this.debug)
57
+ console.log(`[BytemTracker] Using provided visitor ID: ${this.visitorId}`);
66
58
  }
67
59
  else {
68
60
  this.visitorId = await (0, storage_1.getItem)(storage_1.StorageKeys.VISITOR_ID);
69
61
  if (!this.visitorId) {
70
62
  this.visitorId = (0, device_1.generateUUID)();
71
63
  await (0, storage_1.setItem)(storage_1.StorageKeys.VISITOR_ID, this.visitorId);
64
+ if (this.debug)
65
+ console.log(`[BytemTracker] Generated new visitor ID: ${this.visitorId}`);
66
+ }
67
+ else {
68
+ if (this.debug)
69
+ console.log(`[BytemTracker] Restored visitor ID: ${this.visitorId}`);
72
70
  }
73
71
  }
74
- // Initialize Session
75
- await (0, session_1.initSession)();
76
- if (!this.config.endpoint) {
77
- this.config.endpoint = this.DEFAULT_ENDPOINT;
72
+ // Initialize Device ID type
73
+ if (config.deviceId) {
74
+ this.deviceIdType = 0; // Developer set
75
+ await (0, storage_1.setItem)(storage_1.StorageKeys.DEVICE_ID, config.deviceId);
76
+ if (this.debug)
77
+ console.log(`[BytemTracker] Using developer-set device ID: ${config.deviceId}`);
78
78
  }
79
- if (config.debug) {
80
- console.log('[BytemTracker] Initialized with config:', this.config);
81
- console.log('[BytemTracker] Visitor ID:', this.visitorId);
79
+ else {
80
+ this.deviceIdType = 1; // Auto generated (using visitor ID or platform ID logic)
81
+ }
82
+ this.isInitialized = true;
83
+ if (this.debug) {
84
+ console.log('[BytemTracker] Initialized ✅');
85
+ console.log(` app_key: ${this.appKey}`);
86
+ console.log(` base_url: ${this.baseUrl}`);
87
+ console.log(` visitor_id: ${this.visitorId}`);
88
+ if (this.appScheme)
89
+ console.log(` app_scheme: ${this.appScheme}`);
90
+ }
91
+ // Begin session
92
+ try {
93
+ await this.beginSession();
94
+ }
95
+ catch (e) {
96
+ if (this.debug) {
97
+ console.warn('[BytemTracker] Failed to begin session during init:', e);
98
+ }
99
+ this.sessionStarted = false;
100
+ }
101
+ }
102
+ ensureInitialized() {
103
+ if (!this.isInitialized) {
104
+ throw new Error('[BytemTracker] Not initialized. Call await init() first.');
82
105
  }
83
106
  }
84
- async track(payload) {
85
- if (!this.isInitialized || !this.config || !this.visitorId) {
86
- console.error('[BytemTracker] Not initialized. Call await init() first.');
107
+ async beginSession(force = false) {
108
+ this.ensureInitialized();
109
+ if (this.sessionStarted) {
110
+ if (this.debug)
111
+ console.log('[BytemTracker] Session already started, skipping');
87
112
  return;
88
113
  }
89
- const baseParams = await (0, base_1.buildBaseRequestParams)(this.config.appId, this.visitorId);
90
- const finalData = Object.assign(Object.assign({}, baseParams), payload);
91
- if (this.config.debug) {
92
- console.log('[BytemTracker] Tracking event:', finalData);
93
- }
94
- if (this.config.endpoint) {
95
- let requestUrl = this.config.endpoint;
96
- try {
97
- const urlObj = new URL(this.config.endpoint);
98
- const hasEndpointPath = urlObj.pathname && urlObj.pathname !== '/';
99
- const pathToUse = this.config.path;
100
- if (pathToUse) {
101
- urlObj.pathname = pathToUse.startsWith('/') ? pathToUse : '/' + pathToUse;
102
- requestUrl = urlObj.toString();
103
- }
104
- else if (!hasEndpointPath) {
105
- urlObj.pathname = '/i';
106
- requestUrl = urlObj.toString();
114
+ try {
115
+ this.sessionStarted = true;
116
+ this.lastBeat = Date.now();
117
+ // Check session cookie
118
+ const sessionKey = `${this.appKey}/cly_session`;
119
+ const expireTimestampStr = await (0, storage_1.getItem)(sessionKey);
120
+ const expireTimestamp = expireTimestampStr ? parseInt(expireTimestampStr, 10) : null;
121
+ const currentTimestamp = Date.now();
122
+ const shouldSendRequest = force ||
123
+ !this.useSessionCookie ||
124
+ expireTimestamp === null ||
125
+ expireTimestamp <= currentTimestamp;
126
+ if (!shouldSendRequest) {
127
+ if (this.debug)
128
+ console.log('[BytemTracker] Session cookie still valid, skipping begin_session request');
129
+ // Update cookie
130
+ await (0, storage_1.setItem)(sessionKey, (currentTimestamp + this.sessionCookieTimeout * 60 * 1000).toString());
131
+ return;
132
+ }
133
+ const deviceId = await this.getRealDeviceId();
134
+ const metrics = await this.getMetrics();
135
+ const body = await this.buildBaseRequestParams(deviceId);
136
+ body['begin_session'] = '1';
137
+ body['metrics'] = JSON.stringify(metrics);
138
+ await this.sendRequest(this.apiPath, body, 'Begin Session');
139
+ await (0, storage_1.setItem)(sessionKey, (currentTimestamp + this.sessionCookieTimeout * 60 * 1000).toString());
140
+ }
141
+ catch (e) {
142
+ this.sessionStarted = false;
143
+ if (this.debug)
144
+ console.error('[BytemTracker] Error in beginSession:', e);
145
+ }
146
+ }
147
+ async sessionDuration(sec) {
148
+ this.ensureInitialized();
149
+ if (!this.sessionStarted) {
150
+ if (this.debug)
151
+ console.log('[BytemTracker] Session not started, skipping session_duration');
152
+ return;
153
+ }
154
+ const deviceId = await this.getRealDeviceId();
155
+ const body = await this.buildBaseRequestParams(deviceId);
156
+ body['session_duration'] = sec.toString();
157
+ await this.sendRequest(this.apiPath, body, 'Session Duration');
158
+ this.lastBeat = Date.now();
159
+ await this.extendSession();
160
+ }
161
+ async endSession(sec, force = false) {
162
+ this.ensureInitialized();
163
+ if (!this.sessionStarted) {
164
+ if (this.debug)
165
+ console.log('[BytemTracker] Session not started, skipping end_session');
166
+ return;
167
+ }
168
+ const currentTimestamp = Date.now();
169
+ const sessionSec = sec !== null && sec !== void 0 ? sec : (this.lastBeat ? (currentTimestamp - this.lastBeat) / 1000 : 0);
170
+ if (this.debug)
171
+ console.log(`[BytemTracker] Ending session with duration: ${sessionSec} seconds`);
172
+ if (this.useSessionCookie && !force) {
173
+ await this.sessionDuration(sessionSec);
174
+ this.sessionStarted = false;
175
+ return;
176
+ }
177
+ const deviceId = await this.getRealDeviceId();
178
+ const body = await this.buildBaseRequestParams(deviceId);
179
+ body['end_session'] = '1';
180
+ body['session_duration'] = sessionSec.toString();
181
+ await this.sendRequest(this.apiPath, body, 'End Session');
182
+ this.sessionStarted = false;
183
+ }
184
+ async trackSessions() {
185
+ this.ensureInitialized();
186
+ if (this.debug)
187
+ console.log('[BytemTracker] trackSessions called');
188
+ if (this.sessionStarted && this.lastBeat) {
189
+ const duration = (Date.now() - this.lastBeat) / 1000;
190
+ if (duration > 0) {
191
+ if (this.debug)
192
+ console.log(`[BytemTracker] Session already started, sending session_duration: ${duration}s`);
193
+ await this.sessionDuration(duration);
194
+ }
195
+ }
196
+ else {
197
+ await this.beginSession();
198
+ }
199
+ this.startTime();
200
+ if (this.debug)
201
+ console.log('[BytemTracker] Session tracking started');
202
+ }
203
+ startTime() {
204
+ if (!this.trackTime) {
205
+ this.trackTime = true;
206
+ const now = Date.now();
207
+ if (this.lastBeat && this.storedDuration > 0) {
208
+ this.lastBeat = now - this.storedDuration;
209
+ this.storedDuration = 0;
210
+ }
211
+ else {
212
+ this.lastBeat = now;
213
+ }
214
+ if (this.lastViewStoredDuration > 0 && this.lastViewTime > 0) {
215
+ this.lastViewTime = now - this.lastViewStoredDuration;
216
+ this.lastViewStoredDuration = 0;
217
+ }
218
+ else {
219
+ this.lastViewTime = now;
220
+ }
221
+ if (this.debug)
222
+ console.log('[BytemTracker] Time tracking started');
223
+ this.extendSession();
224
+ }
225
+ }
226
+ stopTime() {
227
+ if (this.trackTime) {
228
+ this.trackTime = false;
229
+ const now = Date.now();
230
+ if (this.lastBeat) {
231
+ this.storedDuration = now - this.lastBeat;
232
+ }
233
+ if (this.lastViewTime > 0) {
234
+ this.lastViewStoredDuration = now - this.lastViewTime;
235
+ }
236
+ if (this.debug)
237
+ console.log('[BytemTracker] Time tracking stopped');
238
+ }
239
+ }
240
+ async trackEvent(eventKey, segmentation, count = 1, sum, duration) {
241
+ this.ensureInitialized();
242
+ const deviceId = await this.getRealDeviceId();
243
+ const now = new Date();
244
+ const event = {
245
+ key: eventKey,
246
+ count: count,
247
+ segmentation: segmentation,
248
+ timestamp: now.getTime(),
249
+ hour: now.getHours(),
250
+ dow: now.getDay() === 0 ? 0 : now.getDay(), // JS getDay: 0=Sun, Dart: 7=Sun but mapped to 0. 0-6.
251
+ };
252
+ if (sum !== undefined)
253
+ event.sum = sum;
254
+ if (duration !== undefined)
255
+ event.dur = duration;
256
+ const body = await this.buildBaseRequestParams(deviceId);
257
+ body['events'] = JSON.stringify([event]);
258
+ await this.sendRequest(this.apiPath, body, 'Event');
259
+ }
260
+ async trackPageview(current, referrer) {
261
+ await this.reportViewDuration();
262
+ const segmentation = {
263
+ name: current,
264
+ current: current,
265
+ referrer: '',
266
+ };
267
+ if (referrer)
268
+ segmentation.referrer = referrer;
269
+ let duration;
270
+ if (this.lastViewTime > 0) {
271
+ const now = Date.now();
272
+ duration = this.trackTime
273
+ ? (now - this.lastViewTime) / 1000
274
+ : this.lastViewStoredDuration / 1000;
275
+ }
276
+ this.lastViewTime = Date.now();
277
+ if (!this.trackTime) {
278
+ this.lastViewStoredDuration = 0;
279
+ }
280
+ await this.trackEvent(types_1.BytemEventKeys.view, segmentation, 1, undefined, duration);
281
+ }
282
+ async trackGoodsClick(params) {
283
+ this.ensureInitialized();
284
+ const segmentation = {
285
+ gid: params.gid || '',
286
+ skuid: params.skuid || '',
287
+ url: params.url,
288
+ current: params.current || '',
289
+ referrer: params.referrer || '',
290
+ visitor_id: this.visitorId || '',
291
+ };
292
+ if (this.appScheme)
293
+ segmentation.domain = this.appScheme;
294
+ if (params.source)
295
+ segmentation.source = params.source;
296
+ if (params.position)
297
+ segmentation.position = params.position;
298
+ await this.trackEvent('goods_click', segmentation); // Using string literal as Dart does in implementation
299
+ }
300
+ // --- Helpers ---
301
+ async reportViewDuration() {
302
+ if (this.lastViewTime === 0)
303
+ return;
304
+ const now = Date.now();
305
+ const duration = this.trackTime
306
+ ? (now - this.lastViewTime) / 1000
307
+ : this.lastViewStoredDuration / 1000;
308
+ if (duration > 0) {
309
+ if (this.debug)
310
+ console.log(`[BytemTracker] Last page duration: ${duration}s`);
311
+ }
312
+ }
313
+ async getRealDeviceId() {
314
+ // 1. Check developer set ID
315
+ if (this.deviceIdType === 0) {
316
+ const stored = await (0, storage_1.getItem)(storage_1.StorageKeys.DEVICE_ID);
317
+ if (stored)
318
+ return stored;
319
+ }
320
+ // 2. Use visitor ID
321
+ return this.visitorId || (0, device_1.generateUUID)();
322
+ }
323
+ async getMetrics() {
324
+ const metrics = {
325
+ _sdk_name: this.SDK_NAME,
326
+ _sdk_version: this.SDK_VERSION,
327
+ };
328
+ try {
329
+ const info = await (0, device_1.getDeviceInfo)();
330
+ metrics._platform_version = info.osVersion;
331
+ metrics._app_version = this.SDK_VERSION; // Or get actual app version if available
332
+ metrics._resolution = `${Math.round(info.screenWidth)}x${Math.round(info.screenHeight)}`;
333
+ metrics._locale = info.language;
334
+ // _density could be added if available in getDeviceInfo
335
+ }
336
+ catch (e) {
337
+ if (this.debug)
338
+ console.warn('[BytemTracker] Error getting metrics:', e);
339
+ }
340
+ return metrics;
341
+ }
342
+ async buildBaseRequestParams(deviceId) {
343
+ const now = new Date();
344
+ const visitorId = this.visitorId || (0, device_1.generateUUID)();
345
+ const params = {
346
+ app_key: this.appKey,
347
+ device_id: deviceId,
348
+ kid: visitorId,
349
+ visitor_id: visitorId,
350
+ sdk_name: this.SDK_NAME,
351
+ sdk_version: this.SDK_VERSION,
352
+ t: this.deviceIdType.toString(),
353
+ timestamp: now.getTime().toString(),
354
+ hour: now.getHours().toString(),
355
+ dow: (now.getDay() === 0 ? 0 : now.getDay()).toString(),
356
+ };
357
+ // Add location info if available (not implemented yet in this class properties)
358
+ return params;
359
+ }
360
+ async extendSession() {
361
+ if (!this.useSessionCookie)
362
+ return;
363
+ const sessionKey = `${this.appKey}/cly_session`;
364
+ const expireTimestampStr = await (0, storage_1.getItem)(sessionKey);
365
+ const currentTimestamp = Date.now();
366
+ const expireTimestamp = expireTimestampStr ? parseInt(expireTimestampStr, 10) : null;
367
+ if (expireTimestamp === null || expireTimestamp <= currentTimestamp) {
368
+ if (this.debug)
369
+ console.log('[BytemTracker] Session expired, restarting session');
370
+ this.sessionStarted = false;
371
+ await this.beginSession(true);
372
+ }
373
+ else {
374
+ await (0, storage_1.setItem)(sessionKey, (currentTimestamp + this.sessionCookieTimeout * 60 * 1000).toString());
375
+ }
376
+ }
377
+ async sendRequest(path, body, requestName) {
378
+ // Manually constructing URL and using fetch with urlencoded body
379
+ let requestUrl = this.baseUrl;
380
+ try {
381
+ // Handle endpoint/path logic
382
+ const urlObj = new URL(this.baseUrl);
383
+ if (path) {
384
+ if (path.startsWith('http')) {
385
+ requestUrl = path;
107
386
  }
108
387
  else {
109
- requestUrl = this.config.endpoint;
388
+ // If base url has path, append or replace?
389
+ // Dart implementation just concatenates: "$_baseUrl$path"
390
+ // But _baseUrl usually has no trailing slash, and path starts with /
391
+ const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
392
+ const endpointPath = path.startsWith('/') ? path : '/' + path;
393
+ requestUrl = `${base}${endpointPath}`;
110
394
  }
111
395
  }
112
- catch (_a) {
113
- requestUrl = this.config.endpoint;
396
+ }
397
+ catch (e) {
398
+ requestUrl = this.baseUrl + path;
399
+ }
400
+ if (this.debug) {
401
+ console.log(`📤 [BytemTracker] Sending ${requestName}:`);
402
+ console.log(` URL: ${requestUrl}`);
403
+ console.log(` Body:`, body);
404
+ }
405
+ try {
406
+ // Convert body to urlencoded string
407
+ const formBody = Object.keys(body).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(body[key])).join('&');
408
+ const response = await fetch(requestUrl, {
409
+ method: 'POST',
410
+ headers: {
411
+ 'Content-Type': 'application/x-www-form-urlencoded',
412
+ },
413
+ body: formBody,
414
+ });
415
+ if (this.debug) {
416
+ const resText = await response.text();
417
+ console.log(`📥 [BytemTracker] Response (${response.status}): ${resText}`);
114
418
  }
115
- await (0, request_1.sendRequest)(requestUrl, finalData);
419
+ }
420
+ catch (e) {
421
+ if (this.debug)
422
+ console.error(`❌ [BytemTracker] Error sending ${requestName}:`, e);
116
423
  }
117
424
  }
118
- trackViewProduct(product) {
119
- const payload = Business.trackViewProduct(product);
120
- this.track(payload);
425
+ // Wrappers for Business methods to maintain compatibility or you can refactor them too
426
+ async trackViewProduct(product) {
427
+ // Map Product to segmentation
428
+ const segmentation = {
429
+ product_id: product.productId,
430
+ name: product.name,
431
+ price: product.price,
432
+ currency: product.currency,
433
+ category: product.category,
434
+ };
435
+ // In Dart, trackViewProduct sends "view_product" event.
436
+ await this.trackEvent('view_product', segmentation);
121
437
  }
122
- trackCheckOutOrder(order) {
123
- const payload = Business.trackCheckOutOrder(order);
124
- this.track(payload);
438
+ async trackCheckOutOrder(order) {
439
+ // Map Order to segmentation
440
+ const segmentation = {
441
+ order_id: order.orderId,
442
+ total: order.total,
443
+ currency: order.currency,
444
+ // products need to be handled. Dart sends lists of gids, etc.
445
+ // This is a simplification. Ideally we replicate trackCheckOutOrder logic fully.
446
+ };
447
+ await this.trackEvent('check_out_order', segmentation);
448
+ }
449
+ async trackPayOrder(order) {
450
+ const segmentation = {
451
+ order_id: order.orderId,
452
+ total: order.total,
453
+ currency: order.currency,
454
+ };
455
+ await this.trackEvent('pay_order', segmentation);
125
456
  }
126
- trackPayOrder(order) {
127
- const payload = Business.trackPayOrder(order);
128
- this.track(payload);
457
+ async trackUser(userId, traits) {
458
+ // Map to user_details
459
+ // This is a special request in Dart: "user_details" param
460
+ await this.sendUserDetails(userId, traits);
129
461
  }
130
- trackUser(userId, traits) {
131
- const payload = Business.trackUser(userId, traits);
132
- this.track(payload);
462
+ async sendUserDetails(userId, traits) {
463
+ this.ensureInitialized();
464
+ const deviceId = await this.getRealDeviceId();
465
+ const userData = Object.assign({}, traits);
466
+ if (userId) {
467
+ userData.custom = Object.assign(Object.assign({}, userData.custom), { user_id: userId, visitor_id: this.visitorId });
468
+ }
469
+ const body = await this.buildBaseRequestParams(deviceId);
470
+ body['user_details'] = JSON.stringify(userData);
471
+ await this.sendRequest(this.apiPath, body, 'User Details');
133
472
  }
134
473
  }
135
474
  exports.default = BytemTracker.getInstance();
@@ -4,7 +4,18 @@ export interface TrackerConfig {
4
4
  path?: string;
5
5
  debug?: boolean;
6
6
  visitorId?: string;
7
+ deviceId?: string;
8
+ appScheme?: string;
7
9
  }
10
+ export declare const BytemEventKeys: {
11
+ nps: string;
12
+ survey: string;
13
+ starRating: string;
14
+ view: string;
15
+ orientation: string;
16
+ pushAction: string;
17
+ action: string;
18
+ };
8
19
  export interface DeviceInfo {
9
20
  platform: string;
10
21
  os: string;
package/dist/src/types.js CHANGED
@@ -1,2 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BytemEventKeys = void 0;
4
+ exports.BytemEventKeys = {
5
+ nps: "[CLY]_nps",
6
+ survey: "[CLY]_survey",
7
+ starRating: "[CLY]_star_rating",
8
+ view: "[CLY]_view",
9
+ orientation: "[CLY]_orientation",
10
+ pushAction: "[CLY]_push_action",
11
+ action: "[CLY]_action",
12
+ };
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const BytemTracker_1 = __importDefault(require("../src/BytemTracker"));
7
7
  const storage_1 = require("../src/core/storage");
8
8
  const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
9
+ const types_1 = require("../src/types");
9
10
  describe('BytemTracker SDK', () => {
10
11
  const mockConfig = {
11
12
  appId: 'test-app-id',
@@ -14,71 +15,234 @@ describe('BytemTracker SDK', () => {
14
15
  };
15
16
  beforeEach(() => {
16
17
  jest.clearAllMocks();
17
- // Reset singleton instance manually
18
- // @ts-ignore
19
- BytemTracker_1.default.instance = undefined;
18
+ // Reset singleton instance state manually
20
19
  // @ts-ignore
21
20
  BytemTracker_1.default.isInitialized = false;
22
21
  // @ts-ignore
23
- BytemTracker_1.default.config = null;
22
+ BytemTracker_1.default.appKey = null;
23
+ // @ts-ignore
24
+ BytemTracker_1.default.baseUrl = 'https://tracking.server.bytecon.com';
24
25
  // @ts-ignore
25
26
  BytemTracker_1.default.visitorId = null;
27
+ // @ts-ignore
28
+ BytemTracker_1.default.sessionStarted = false;
29
+ // @ts-ignore
30
+ BytemTracker_1.default.lastBeat = null;
31
+ // @ts-ignore
32
+ BytemTracker_1.default.trackTime = true;
33
+ // @ts-ignore
34
+ BytemTracker_1.default.storedDuration = 0;
35
+ // @ts-ignore
36
+ BytemTracker_1.default.lastViewTime = 0;
37
+ // @ts-ignore
38
+ BytemTracker_1.default.lastViewStoredDuration = 0;
39
+ // Clear storage mocks
40
+ // @ts-ignore
41
+ async_storage_1.default.clear();
26
42
  });
27
- it('should initialize correctly', async () => {
28
- await BytemTracker_1.default.init(mockConfig);
29
- // Verify Storage was checked for Visitor ID
30
- expect(async_storage_1.default.getItem).toHaveBeenCalledWith(storage_1.StorageKeys.VISITOR_ID);
31
- // Verify Session was initialized (checks storage)
32
- expect(async_storage_1.default.getItem).toHaveBeenCalledWith(storage_1.StorageKeys.SESSION_ID);
43
+ describe('Initialization', () => {
44
+ it('should initialize correctly', async () => {
45
+ await BytemTracker_1.default.init(mockConfig);
46
+ // Verify Storage was checked for Visitor ID
47
+ expect(async_storage_1.default.getItem).toHaveBeenCalledWith(storage_1.StorageKeys.VISITOR_ID);
48
+ // Verify Session was initialized (checks storage)
49
+ expect(async_storage_1.default.getItem).toHaveBeenCalledWith(`${mockConfig.appId}/cly_session`);
50
+ });
51
+ it('should use default endpoint when not provided', async () => {
52
+ await BytemTracker_1.default.init({
53
+ appId: 'test-app-id',
54
+ // endpoint is omitted
55
+ debug: true,
56
+ });
57
+ await BytemTracker_1.default.trackEvent('test_event', {});
58
+ expect(global.fetch).toHaveBeenCalled();
59
+ const calls = global.fetch.mock.calls;
60
+ // Check any call, they should all use the default endpoint
61
+ const url = calls[0][0];
62
+ expect(url).toBe('https://tracking.server.bytecon.com/i');
63
+ });
64
+ it('should override endpoint path when path is configured', async () => {
65
+ await BytemTracker_1.default.init({
66
+ appId: 'test-app-id-2',
67
+ endpoint: 'https://api.example.com/track',
68
+ path: '/collect',
69
+ debug: true,
70
+ });
71
+ await BytemTracker_1.default.trackEvent('test_event', {});
72
+ expect(global.fetch).toHaveBeenCalled();
73
+ const calls = global.fetch.mock.calls;
74
+ const url = calls[0][0];
75
+ expect(url).toBe('https://api.example.com/track/collect');
76
+ });
33
77
  });
34
- it('should track an event', async () => {
35
- // Ensure initialized (singleton persists)
36
- await BytemTracker_1.default.init(mockConfig);
37
- const eventName = 'test_event';
38
- const eventParams = { foo: 'bar' };
39
- // @ts-ignore - Accessing private method for testing or just use public track methods
40
- // Since track is private, we'll use a public method like trackUser or just trust the public API
41
- // Let's use trackUser as a proxy for generic tracking
42
- BytemTracker_1.default.trackUser('user-123', { age: 25 });
43
- // Wait for async operations if any (track is async but void return in public API might hide it)
44
- // In BytemTracker.ts, trackUser calls track, which is async but not awaited by caller.
45
- // We need to wait for promises to resolve.
46
- await new Promise(resolve => setTimeout(resolve, 0));
47
- expect(global.fetch).toHaveBeenCalledTimes(1);
48
- const fetchCall = global.fetch.mock.calls[0];
49
- const url = fetchCall[0];
50
- const options = fetchCall[1];
51
- expect(url).toBe(mockConfig.endpoint);
52
- expect(options.method).toBe('POST');
53
- const body = JSON.parse(options.body);
54
- expect(body.app_id).toBe(mockConfig.appId);
55
- // expect(body.event).toBe('identify');
78
+ describe('Event Tracking', () => {
79
+ beforeEach(async () => {
80
+ await BytemTracker_1.default.init(mockConfig);
81
+ });
82
+ it('should track a simple event', async () => {
83
+ const eventName = 'test_event';
84
+ const segmentation = { foo: 'bar' };
85
+ await BytemTracker_1.default.trackEvent(eventName, segmentation);
86
+ expect(global.fetch).toHaveBeenCalled();
87
+ const calls = global.fetch.mock.calls;
88
+ const eventCall = calls.find(call => call[1].body && call[1].body.includes('events='));
89
+ expect(eventCall).toBeDefined();
90
+ const options = eventCall[1];
91
+ const body = options.body;
92
+ expect(body).toContain(`app_key=${mockConfig.appId}`);
93
+ expect(body).toContain('events=%5B%7B%22key%22%3A%22test_event%22');
94
+ });
95
+ it('should track event with count and sum', async () => {
96
+ const eventName = 'purchase';
97
+ const count = 3;
98
+ const sum = 99.99;
99
+ const segmentation = { item: 'apple' };
100
+ await BytemTracker_1.default.trackEvent(eventName, segmentation, count, sum);
101
+ const calls = global.fetch.mock.calls;
102
+ const eventCall = calls.find(call => call[1].body && call[1].body.includes('events='));
103
+ const body = decodeURIComponent(eventCall[1].body);
104
+ const events = JSON.parse(new URLSearchParams(eventCall[1].body).get('events') || '[]');
105
+ expect(events[0]).toMatchObject({
106
+ key: eventName,
107
+ count: count,
108
+ sum: sum,
109
+ segmentation: segmentation
110
+ });
111
+ });
112
+ it('should track event with duration', async () => {
113
+ const eventName = 'video_watched';
114
+ const duration = 120; // seconds
115
+ await BytemTracker_1.default.trackEvent(eventName, {}, 1, undefined, duration);
116
+ const calls = global.fetch.mock.calls;
117
+ const eventCall = calls.find(call => call[1].body && call[1].body.includes('events='));
118
+ const events = JSON.parse(new URLSearchParams(eventCall[1].body).get('events') || '[]');
119
+ expect(events[0].dur).toBe(duration);
120
+ });
121
+ });
122
+ describe('Page View Tracking', () => {
123
+ beforeEach(async () => {
124
+ await BytemTracker_1.default.init(mockConfig);
125
+ });
126
+ it('should track page view', async () => {
127
+ const pageName = '/home';
128
+ await BytemTracker_1.default.trackPageview(pageName);
129
+ const calls = global.fetch.mock.calls;
130
+ const eventCall = calls.find(call => call[1].body && call[1].body.includes(`%22key%22%3A%22${encodeURIComponent(types_1.BytemEventKeys.view)}%22`));
131
+ expect(eventCall).toBeDefined();
132
+ const events = JSON.parse(new URLSearchParams(eventCall[1].body).get('events') || '[]');
133
+ expect(events[0].segmentation).toMatchObject({
134
+ name: pageName,
135
+ current: pageName,
136
+ referrer: ''
137
+ });
138
+ });
139
+ it('should track page view with referrer', async () => {
140
+ const pageName = '/product/123';
141
+ const referrer = '/home';
142
+ await BytemTracker_1.default.trackPageview(pageName, referrer);
143
+ const calls = global.fetch.mock.calls;
144
+ // Get the last call which should be the page view
145
+ const eventCall = calls[calls.length - 1];
146
+ const events = JSON.parse(new URLSearchParams(eventCall[1].body).get('events') || '[]');
147
+ expect(events[0].segmentation).toMatchObject({
148
+ name: pageName,
149
+ current: pageName,
150
+ referrer: referrer
151
+ });
152
+ });
153
+ });
154
+ describe('User Tracking', () => {
155
+ beforeEach(async () => {
156
+ await BytemTracker_1.default.init(mockConfig);
157
+ });
158
+ it('should track user details', async () => {
159
+ const userId = 'user_123';
160
+ const userTraits = {
161
+ name: 'John Doe',
162
+ email: 'john@example.com',
163
+ custom: {
164
+ plan: 'premium'
165
+ }
166
+ };
167
+ await BytemTracker_1.default.trackUser(userId, userTraits);
168
+ const calls = global.fetch.mock.calls;
169
+ const userCall = calls.find(call => call[1].body && call[1].body.includes('user_details='));
170
+ expect(userCall).toBeDefined();
171
+ const userDetails = JSON.parse(new URLSearchParams(userCall[1].body).get('user_details') || '{}');
172
+ expect(userDetails).toMatchObject({
173
+ name: 'John Doe',
174
+ email: 'john@example.com',
175
+ custom: {
176
+ plan: 'premium',
177
+ user_id: userId
178
+ }
179
+ });
180
+ });
56
181
  });
57
- it('should use default endpoint when not provided', async () => {
58
- await BytemTracker_1.default.init({
59
- appId: 'test-app-id',
60
- // endpoint is omitted
61
- debug: true,
62
- });
63
- BytemTracker_1.default.trackUser('user-123', { age: 25 });
64
- await new Promise(resolve => setTimeout(resolve, 0));
65
- expect(global.fetch).toHaveBeenCalledTimes(1);
66
- const fetchCall = global.fetch.mock.calls[0];
67
- const url = fetchCall[0];
68
- expect(url).toBe('https://tracking.server.bytecon.com/i');
182
+ describe('Session Management', () => {
183
+ beforeEach(async () => {
184
+ await BytemTracker_1.default.init(mockConfig);
185
+ });
186
+ it('should track session start', async () => {
187
+ await BytemTracker_1.default.trackSessions();
188
+ const calls = global.fetch.mock.calls;
189
+ const sessionCall = calls.find(call => call[1].body && call[1].body.includes('begin_session=1'));
190
+ expect(sessionCall).toBeDefined();
191
+ });
192
+ it('should track session end', async () => {
193
+ // First start session
194
+ await BytemTracker_1.default.trackSessions();
195
+ // Then end session
196
+ await BytemTracker_1.default.endSession(undefined, true); // force=true to bypass cookie check for test
197
+ const calls = global.fetch.mock.calls;
198
+ const endSessionCall = calls.find(call => call[1].body && call[1].body.includes('end_session=1'));
199
+ expect(endSessionCall).toBeDefined();
200
+ const body = new URLSearchParams(endSessionCall[1].body);
201
+ expect(body.has('session_duration')).toBe(true);
202
+ });
69
203
  });
70
- it('should override endpoint path when path is configured', async () => {
71
- await BytemTracker_1.default.init({
72
- appId: 'test-app-id-2',
73
- endpoint: 'https://api.example.com/track',
74
- path: '/collect',
75
- debug: true,
76
- });
77
- BytemTracker_1.default.trackUser('user-456', { age: 30 });
78
- await new Promise(resolve => setTimeout(resolve, 0));
79
- expect(global.fetch).toHaveBeenCalledTimes(1);
80
- const fetchCall = global.fetch.mock.calls[0];
81
- const url = fetchCall[0];
82
- expect(url).toBe('https://api.example.com/collect');
204
+ describe('E-commerce Tracking', () => {
205
+ beforeEach(async () => {
206
+ await BytemTracker_1.default.init(mockConfig);
207
+ });
208
+ it('should track checkout order', async () => {
209
+ const order = {
210
+ orderId: 'order_123',
211
+ total: 99.99,
212
+ currency: 'USD',
213
+ products: [
214
+ { productId: 'prod_1', price: 50, quantity: 1 },
215
+ { productId: 'prod_2', price: 49.99, quantity: 1 }
216
+ ]
217
+ };
218
+ await BytemTracker_1.default.trackCheckOutOrder(order);
219
+ const calls = global.fetch.mock.calls;
220
+ const eventCall = calls.find(call => call[1].body && call[1].body.includes('events='));
221
+ const events = JSON.parse(new URLSearchParams(eventCall[1].body).get('events') || '[]');
222
+ expect(events[0].key).toBe('check_out_order');
223
+ expect(events[0].segmentation).toMatchObject({
224
+ order_id: 'order_123',
225
+ total: 99.99,
226
+ currency: 'USD'
227
+ });
228
+ });
229
+ it('should track pay order', async () => {
230
+ const order = {
231
+ orderId: 'order_123',
232
+ total: 99.99,
233
+ currency: 'USD',
234
+ products: []
235
+ };
236
+ await BytemTracker_1.default.trackPayOrder(order);
237
+ const calls = global.fetch.mock.calls;
238
+ const eventCall = calls.find(call => call[1].body && call[1].body.includes('events='));
239
+ const events = JSON.parse(new URLSearchParams(eventCall[1].body).get('events') || '[]');
240
+ expect(events[0].key).toBe('pay_order');
241
+ expect(events[0].segmentation).toMatchObject({
242
+ order_id: 'order_123',
243
+ total: 99.99,
244
+ currency: 'USD'
245
+ });
246
+ });
83
247
  });
84
248
  });
@@ -13,7 +13,22 @@ describe('Debug Scenarios', () => {
13
13
  };
14
14
  beforeEach(async () => {
15
15
  jest.clearAllMocks();
16
+ // Reset singleton instance state manually
17
+ // @ts-ignore
18
+ BytemTracker_1.default.isInitialized = false;
19
+ // @ts-ignore
20
+ BytemTracker_1.default.appKey = null;
21
+ // @ts-ignore
22
+ BytemTracker_1.default.baseUrl = 'https://tracking.server.bytecon.com';
23
+ // @ts-ignore
24
+ BytemTracker_1.default.visitorId = null;
25
+ // @ts-ignore
26
+ BytemTracker_1.default.sessionStarted = false;
27
+ // @ts-ignore
28
+ BytemTracker_1.default.lastBeat = null;
16
29
  // Simulate fresh start
30
+ // @ts-ignore
31
+ async_storage_1.default.clear();
17
32
  async_storage_1.default.getItem.mockResolvedValue(null);
18
33
  await BytemTracker_1.default.init(mockConfig);
19
34
  });
@@ -48,11 +63,28 @@ describe('Debug Scenarios', () => {
48
63
  });
49
64
  await new Promise(resolve => setTimeout(resolve, 0));
50
65
  // Assertions to verify flow
51
- expect(global.fetch).toHaveBeenCalledTimes(3);
66
+ // Expect 4 calls: begin_session, view_product, user_details, check_out_order
67
+ expect(global.fetch).toHaveBeenCalledTimes(4);
52
68
  const calls = global.fetch.mock.calls;
53
- // Check Event Types
54
- const events = calls.map((call) => JSON.parse(call[1].body).event);
55
- console.log('>>> Captured Events:', events);
56
- expect(events).toEqual(['view_product', 'identify', 'checkout_order']);
69
+ // Check Request Types
70
+ const requestTypes = calls.map((call) => {
71
+ const body = call[1].body;
72
+ if (body.includes('begin_session=1'))
73
+ return 'begin_session';
74
+ if (body.includes('user_details='))
75
+ return 'identify';
76
+ if (body.includes('events=')) {
77
+ // Extract event key
78
+ const match = body.match(/events=(.*?)($|&)/);
79
+ if (match) {
80
+ const eventsJson = decodeURIComponent(match[1]);
81
+ const events = JSON.parse(eventsJson);
82
+ return events[0].key;
83
+ }
84
+ }
85
+ return 'unknown';
86
+ });
87
+ console.log('>>> Captured Requests:', requestTypes);
88
+ expect(requestTypes).toEqual(['begin_session', 'view_product', 'identify', 'check_out_order']);
57
89
  });
58
90
  });
@@ -53,4 +53,5 @@ global.fetch = jest.fn(() => Promise.resolve({
53
53
  status: 200,
54
54
  statusText: 'OK',
55
55
  json: () => Promise.resolve({}),
56
+ text: () => Promise.resolve('{"status":"success"}'),
56
57
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytem/bytem-tracker-app",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Bytem Tracker SDK for React Native",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",