@axyle/expo-sdk 1.0.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.
- package/CONFIG.md +117 -0
- package/README.md +534 -0
- package/dist/client.d.ts +93 -0
- package/dist/client.js +416 -0
- package/dist/config.example.d.ts +8 -0
- package/dist/config.example.js +13 -0
- package/dist/configLoader.d.ts +10 -0
- package/dist/configLoader.js +22 -0
- package/dist/constants.d.ts +38 -0
- package/dist/constants.js +42 -0
- package/dist/context.d.ts +36 -0
- package/dist/context.js +216 -0
- package/dist/core.d.ts +9 -0
- package/dist/core.js +45 -0
- package/dist/hooks/useFeatureTracking.d.ts +34 -0
- package/dist/hooks/useFeatureTracking.js +60 -0
- package/dist/hooks/useOnboardingTracking.d.ts +85 -0
- package/dist/hooks/useOnboardingTracking.js +172 -0
- package/dist/hooks/useScreenTracking.d.ts +23 -0
- package/dist/hooks/useScreenTracking.js +52 -0
- package/dist/hooks/useScrollTracking.d.ts +39 -0
- package/dist/hooks/useScrollTracking.js +146 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.js +171 -0
- package/dist/integrations/expoRouter.d.ts +42 -0
- package/dist/integrations/expoRouter.js +66 -0
- package/dist/integrations/reactNavigation.d.ts +27 -0
- package/dist/integrations/reactNavigation.js +101 -0
- package/dist/queue.d.ts +52 -0
- package/dist/queue.js +143 -0
- package/dist/session.d.ts +44 -0
- package/dist/session.js +139 -0
- package/dist/sessionAnalytics.d.ts +43 -0
- package/dist/sessionAnalytics.js +74 -0
- package/dist/storage.d.ts +67 -0
- package/dist/storage.js +210 -0
- package/dist/transport.d.ts +41 -0
- package/dist/transport.js +158 -0
- package/dist/types.d.ts +102 -0
- package/dist/types.js +5 -0
- package/dist/utils.d.ts +39 -0
- package/dist/utils.js +116 -0
- package/package.json +44 -0
package/dist/storage.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Storage layer using AsyncStorage for persistence
|
|
4
|
+
* Handles event queue, user IDs, session data, and preferences
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.Storage = void 0;
|
|
11
|
+
const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
|
|
12
|
+
const constants_1 = require("./constants");
|
|
13
|
+
const utils_1 = require("./utils");
|
|
14
|
+
class Storage {
|
|
15
|
+
/**
|
|
16
|
+
* Get all queued events
|
|
17
|
+
*/
|
|
18
|
+
static async getQueuedEvents() {
|
|
19
|
+
try {
|
|
20
|
+
const data = await async_storage_1.default.getItem(constants_1.STORAGE_KEYS.EVENTS_QUEUE);
|
|
21
|
+
if (!data)
|
|
22
|
+
return [];
|
|
23
|
+
return JSON.parse(data);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error('[Axyle] Failed to get queued events:', error);
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Save events to queue
|
|
32
|
+
*/
|
|
33
|
+
static async saveQueuedEvents(events) {
|
|
34
|
+
try {
|
|
35
|
+
await async_storage_1.default.setItem(constants_1.STORAGE_KEYS.EVENTS_QUEUE, (0, utils_1.safeStringify)(events));
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.error('[Axyle] Failed to save queued events:', error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Add event to queue
|
|
43
|
+
*/
|
|
44
|
+
static async addEventToQueue(event) {
|
|
45
|
+
try {
|
|
46
|
+
const events = await this.getQueuedEvents();
|
|
47
|
+
events.push(event);
|
|
48
|
+
await this.saveQueuedEvents(events);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error('[Axyle] Failed to add event to queue:', error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Remove events from queue
|
|
56
|
+
*/
|
|
57
|
+
static async removeEventsFromQueue(eventIds) {
|
|
58
|
+
try {
|
|
59
|
+
const events = await this.getQueuedEvents();
|
|
60
|
+
const filtered = events.filter((e) => !eventIds.includes(e.id));
|
|
61
|
+
await this.saveQueuedEvents(filtered);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error('[Axyle] Failed to remove events from queue:', error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Clear all queued events
|
|
69
|
+
*/
|
|
70
|
+
static async clearQueue() {
|
|
71
|
+
try {
|
|
72
|
+
await async_storage_1.default.removeItem(constants_1.STORAGE_KEYS.EVENTS_QUEUE);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.error('[Axyle] Failed to clear queue:', error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get anonymous ID (create if doesn't exist)
|
|
80
|
+
*/
|
|
81
|
+
static async getAnonymousId() {
|
|
82
|
+
try {
|
|
83
|
+
let anonymousId = await async_storage_1.default.getItem(constants_1.STORAGE_KEYS.ANONYMOUS_ID);
|
|
84
|
+
if (!anonymousId) {
|
|
85
|
+
// Generate new anonymous ID
|
|
86
|
+
anonymousId = `anon_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
87
|
+
await async_storage_1.default.setItem(constants_1.STORAGE_KEYS.ANONYMOUS_ID, anonymousId);
|
|
88
|
+
}
|
|
89
|
+
return anonymousId;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error('[Axyle] Failed to get anonymous ID:', error);
|
|
93
|
+
// Fallback to in-memory ID
|
|
94
|
+
return `anon_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get user ID
|
|
99
|
+
*/
|
|
100
|
+
static async getUserId() {
|
|
101
|
+
try {
|
|
102
|
+
return await async_storage_1.default.getItem(constants_1.STORAGE_KEYS.USER_ID);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error('[Axyle] Failed to get user ID:', error);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Set user ID
|
|
111
|
+
*/
|
|
112
|
+
static async setUserId(userId) {
|
|
113
|
+
try {
|
|
114
|
+
await async_storage_1.default.setItem(constants_1.STORAGE_KEYS.USER_ID, userId);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error('[Axyle] Failed to set user ID:', error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Clear user ID (on reset/logout)
|
|
122
|
+
*/
|
|
123
|
+
static async clearUserId() {
|
|
124
|
+
try {
|
|
125
|
+
await async_storage_1.default.removeItem(constants_1.STORAGE_KEYS.USER_ID);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.error('[Axyle] Failed to clear user ID:', error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get session data
|
|
133
|
+
*/
|
|
134
|
+
static async getSessionData() {
|
|
135
|
+
try {
|
|
136
|
+
const data = await async_storage_1.default.getItem(constants_1.STORAGE_KEYS.SESSION_DATA);
|
|
137
|
+
if (!data)
|
|
138
|
+
return null;
|
|
139
|
+
return JSON.parse(data);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.error('[Axyle] Failed to get session data:', error);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Save session data
|
|
148
|
+
*/
|
|
149
|
+
static async saveSessionData(session) {
|
|
150
|
+
try {
|
|
151
|
+
await async_storage_1.default.setItem(constants_1.STORAGE_KEYS.SESSION_DATA, (0, utils_1.safeStringify)(session));
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.error('[Axyle] Failed to save session data:', error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Clear session data
|
|
159
|
+
*/
|
|
160
|
+
static async clearSessionData() {
|
|
161
|
+
try {
|
|
162
|
+
await async_storage_1.default.removeItem(constants_1.STORAGE_KEYS.SESSION_DATA);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.error('[Axyle] Failed to clear session data:', error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Check if user has opted out
|
|
170
|
+
*/
|
|
171
|
+
static async isOptedOut() {
|
|
172
|
+
try {
|
|
173
|
+
const value = await async_storage_1.default.getItem(constants_1.STORAGE_KEYS.OPT_OUT);
|
|
174
|
+
return value === 'true';
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
console.error('[Axyle] Failed to check opt-out status:', error);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Set opt-out status
|
|
183
|
+
*/
|
|
184
|
+
static async setOptOut(optOut) {
|
|
185
|
+
try {
|
|
186
|
+
await async_storage_1.default.setItem(constants_1.STORAGE_KEYS.OPT_OUT, optOut ? 'true' : 'false');
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
console.error('[Axyle] Failed to set opt-out status:', error);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Clear all analytics data
|
|
194
|
+
*/
|
|
195
|
+
static async clearAll() {
|
|
196
|
+
try {
|
|
197
|
+
await async_storage_1.default.multiRemove([
|
|
198
|
+
constants_1.STORAGE_KEYS.EVENTS_QUEUE,
|
|
199
|
+
constants_1.STORAGE_KEYS.USER_ID,
|
|
200
|
+
constants_1.STORAGE_KEYS.SESSION_DATA,
|
|
201
|
+
constants_1.STORAGE_KEYS.OPT_OUT,
|
|
202
|
+
// Keep anonymous ID for continuity
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
console.error('[Axyle] Failed to clear all data:', error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
exports.Storage = Storage;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport layer for sending events to the analytics API
|
|
3
|
+
* Handles batching, retry logic with exponential backoff
|
|
4
|
+
*/
|
|
5
|
+
import { AnalyticsEvent } from "./types";
|
|
6
|
+
export interface TransportConfig {
|
|
7
|
+
apiKey: string;
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
debug: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class Transport {
|
|
12
|
+
private config;
|
|
13
|
+
private logger;
|
|
14
|
+
constructor(config: TransportConfig, logger: any);
|
|
15
|
+
/**
|
|
16
|
+
* Send a batch of events to the API
|
|
17
|
+
* Returns the IDs of successfully sent events
|
|
18
|
+
*/
|
|
19
|
+
sendBatch(events: AnalyticsEvent[]): Promise<string[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Send a single batch with retry logic
|
|
22
|
+
*/
|
|
23
|
+
private sendBatchWithRetry;
|
|
24
|
+
/**
|
|
25
|
+
* Make HTTP request to the API
|
|
26
|
+
*/
|
|
27
|
+
private makeRequest;
|
|
28
|
+
/**
|
|
29
|
+
* Split events into batches based on size and count limits
|
|
30
|
+
*/
|
|
31
|
+
private createBatches;
|
|
32
|
+
/**
|
|
33
|
+
* Update API key
|
|
34
|
+
*/
|
|
35
|
+
updateApiKey(apiKey: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Update base URL - disabled, always uses production server
|
|
38
|
+
* This method is kept for backward compatibility but does nothing
|
|
39
|
+
*/
|
|
40
|
+
updateBaseUrl(_baseUrl: string): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Transport layer for sending events to the analytics API
|
|
4
|
+
* Handles batching, retry logic with exponential backoff
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.Transport = void 0;
|
|
8
|
+
const constants_1 = require("./constants");
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
|
+
class Transport {
|
|
11
|
+
constructor(config, logger) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.logger = logger;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Send a batch of events to the API
|
|
17
|
+
* Returns the IDs of successfully sent events
|
|
18
|
+
*/
|
|
19
|
+
async sendBatch(events) {
|
|
20
|
+
if (events.length === 0)
|
|
21
|
+
return [];
|
|
22
|
+
// Split into smaller batches if needed
|
|
23
|
+
const batches = this.createBatches(events);
|
|
24
|
+
const sentEventIds = [];
|
|
25
|
+
for (const batch of batches) {
|
|
26
|
+
try {
|
|
27
|
+
const success = await this.sendBatchWithRetry(batch);
|
|
28
|
+
if (success) {
|
|
29
|
+
sentEventIds.push(...batch.map((e) => e.id));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
this.logger.error("Failed to send batch after retries:", error);
|
|
34
|
+
// Continue with next batch even if this one fails
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return sentEventIds;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Send a single batch with retry logic
|
|
41
|
+
*/
|
|
42
|
+
async sendBatchWithRetry(events) {
|
|
43
|
+
let attempt = 0;
|
|
44
|
+
while (attempt < constants_1.RETRY_CONFIG.maxRetries) {
|
|
45
|
+
try {
|
|
46
|
+
const response = await this.makeRequest(events);
|
|
47
|
+
if (response.ok) {
|
|
48
|
+
this.logger.log(`Successfully sent ${events.length} events`);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
// Get error details for better debugging
|
|
52
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
53
|
+
this.logger.error(`API error ${response.status}:`, errorText);
|
|
54
|
+
// Check if error is retryable
|
|
55
|
+
if (response.status >= 400 && response.status < 500) {
|
|
56
|
+
// Client error - don't retry
|
|
57
|
+
this.logger.error(`Client error ${response.status}, not retrying`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
// Server error - retry with backoff
|
|
61
|
+
this.logger.warn(`Server error ${response.status}, retrying...`);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
this.logger.warn(`Network error on attempt ${attempt + 1}:`, error);
|
|
65
|
+
if (error instanceof Error) {
|
|
66
|
+
this.logger.warn(`Error details: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Calculate backoff delay
|
|
70
|
+
if (attempt < constants_1.RETRY_CONFIG.maxRetries - 1) {
|
|
71
|
+
const delay = (0, utils_1.calculateBackoff)(attempt, constants_1.RETRY_CONFIG.initialDelay, constants_1.RETRY_CONFIG.maxDelay, constants_1.RETRY_CONFIG.backoffMultiplier);
|
|
72
|
+
this.logger.log(`Waiting ${delay}ms before retry...`);
|
|
73
|
+
await (0, utils_1.sleep)(delay);
|
|
74
|
+
}
|
|
75
|
+
attempt++;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Make HTTP request to the API
|
|
81
|
+
*/
|
|
82
|
+
async makeRequest(events) {
|
|
83
|
+
const url = `${this.config.baseUrl}/api/events`;
|
|
84
|
+
this.logger.log(`Sending ${events.length} events to ${url}`);
|
|
85
|
+
try {
|
|
86
|
+
const requestBody = {
|
|
87
|
+
events,
|
|
88
|
+
sentAt: new Date().toISOString(),
|
|
89
|
+
};
|
|
90
|
+
const response = await fetch(url, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
"X-API-Key": this.config.apiKey,
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify(requestBody),
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
100
|
+
this.logger.error(`API request failed: ${response.status} ${response.statusText}`, errorText);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.logger.log(`API request successful: ${response.status}`);
|
|
104
|
+
}
|
|
105
|
+
return response;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
this.logger.error("Network error sending events:", error);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Split events into batches based on size and count limits
|
|
114
|
+
*/
|
|
115
|
+
createBatches(events) {
|
|
116
|
+
const batches = [];
|
|
117
|
+
let currentBatch = [];
|
|
118
|
+
let currentBatchSize = 0;
|
|
119
|
+
for (const event of events) {
|
|
120
|
+
const eventSize = (0, utils_1.getObjectSize)(event);
|
|
121
|
+
// Check if adding this event would exceed limits
|
|
122
|
+
if (currentBatch.length >= constants_1.MAX_EVENTS_PER_BATCH ||
|
|
123
|
+
currentBatchSize + eventSize > constants_1.MAX_BATCH_SIZE) {
|
|
124
|
+
// Start new batch
|
|
125
|
+
if (currentBatch.length > 0) {
|
|
126
|
+
batches.push(currentBatch);
|
|
127
|
+
}
|
|
128
|
+
currentBatch = [event];
|
|
129
|
+
currentBatchSize = eventSize;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Add to current batch
|
|
133
|
+
currentBatch.push(event);
|
|
134
|
+
currentBatchSize += eventSize;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Add final batch
|
|
138
|
+
if (currentBatch.length > 0) {
|
|
139
|
+
batches.push(currentBatch);
|
|
140
|
+
}
|
|
141
|
+
return batches;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Update API key
|
|
145
|
+
*/
|
|
146
|
+
updateApiKey(apiKey) {
|
|
147
|
+
this.config.apiKey = apiKey;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Update base URL - disabled, always uses production server
|
|
151
|
+
* This method is kept for backward compatibility but does nothing
|
|
152
|
+
*/
|
|
153
|
+
updateBaseUrl(_baseUrl) {
|
|
154
|
+
// baseUrl is not configurable - always uses production server
|
|
155
|
+
// This method is kept for backward compatibility but does nothing
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
exports.Transport = Transport;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for Axyle Analytics SDK
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* SDK initialization accepts only the API key
|
|
6
|
+
* All other settings use internal defaults
|
|
7
|
+
*/
|
|
8
|
+
export type AxyleInitConfig = string | {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Internal configuration type with all settings
|
|
13
|
+
* Only apiKey comes from user, everything else uses defaults
|
|
14
|
+
*/
|
|
15
|
+
export interface InternalAxyleConfig {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
userId: string;
|
|
18
|
+
environment: "dev" | "prod";
|
|
19
|
+
debug: boolean;
|
|
20
|
+
maxQueueSize: number;
|
|
21
|
+
flushInterval: number;
|
|
22
|
+
sessionTimeout: number;
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
}
|
|
25
|
+
export interface EventProperties {
|
|
26
|
+
[key: string]: string | number | boolean | null | undefined;
|
|
27
|
+
}
|
|
28
|
+
export interface UserTraits {
|
|
29
|
+
[key: string]: string | number | boolean | null | undefined;
|
|
30
|
+
}
|
|
31
|
+
export interface AnalyticsEvent {
|
|
32
|
+
/** Unique event ID */
|
|
33
|
+
id: string;
|
|
34
|
+
/** Event name */
|
|
35
|
+
name: string;
|
|
36
|
+
/** Event properties */
|
|
37
|
+
properties: EventProperties;
|
|
38
|
+
/** Timestamp when event occurred */
|
|
39
|
+
timestamp: number;
|
|
40
|
+
/** User ID (anonymous or identified) */
|
|
41
|
+
userId: string;
|
|
42
|
+
/** Anonymous user ID (persists across sessions) */
|
|
43
|
+
anonymousId: string;
|
|
44
|
+
/** Session ID */
|
|
45
|
+
sessionId: string;
|
|
46
|
+
/** Device metadata */
|
|
47
|
+
context: EventContext;
|
|
48
|
+
/** Schema version */
|
|
49
|
+
schemaVersion: string;
|
|
50
|
+
}
|
|
51
|
+
export interface EventContext {
|
|
52
|
+
/** App information */
|
|
53
|
+
app: {
|
|
54
|
+
name: string;
|
|
55
|
+
version: string;
|
|
56
|
+
build: string;
|
|
57
|
+
namespace: string;
|
|
58
|
+
};
|
|
59
|
+
/** Device information */
|
|
60
|
+
device: {
|
|
61
|
+
id: string | null;
|
|
62
|
+
manufacturer: string | null;
|
|
63
|
+
model: string | null;
|
|
64
|
+
name: string | null;
|
|
65
|
+
type: "PHONE" | "TABLET" | "DESKTOP" | "TV" | "UNKNOWN";
|
|
66
|
+
brand: string | null;
|
|
67
|
+
};
|
|
68
|
+
/** OS information */
|
|
69
|
+
os: {
|
|
70
|
+
name: string;
|
|
71
|
+
version: string;
|
|
72
|
+
};
|
|
73
|
+
/** Screen information */
|
|
74
|
+
screen: {
|
|
75
|
+
width: number;
|
|
76
|
+
height: number;
|
|
77
|
+
density: number;
|
|
78
|
+
};
|
|
79
|
+
/** Locale */
|
|
80
|
+
locale: string;
|
|
81
|
+
/** Timezone */
|
|
82
|
+
timezone: string;
|
|
83
|
+
/** Network information (if available) */
|
|
84
|
+
network?: {
|
|
85
|
+
carrier: string | null;
|
|
86
|
+
wifi: boolean;
|
|
87
|
+
};
|
|
88
|
+
/** Environment */
|
|
89
|
+
environment: "dev" | "prod";
|
|
90
|
+
}
|
|
91
|
+
export interface SessionData {
|
|
92
|
+
sessionId: string;
|
|
93
|
+
startTime: number;
|
|
94
|
+
lastActivityTime: number;
|
|
95
|
+
}
|
|
96
|
+
export interface StorageKeys {
|
|
97
|
+
EVENTS_QUEUE: string;
|
|
98
|
+
ANONYMOUS_ID: string;
|
|
99
|
+
USER_ID: string;
|
|
100
|
+
SESSION_DATA: string;
|
|
101
|
+
OPT_OUT: string;
|
|
102
|
+
}
|
package/dist/types.js
ADDED
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for the SDK
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Generate a UUID v4
|
|
6
|
+
*/
|
|
7
|
+
export declare function generateUUID(): string;
|
|
8
|
+
/**
|
|
9
|
+
* Safely stringify an object, handling circular references
|
|
10
|
+
*/
|
|
11
|
+
export declare function safeStringify(obj: any): string;
|
|
12
|
+
/**
|
|
13
|
+
* Calculate size of an object in bytes
|
|
14
|
+
*/
|
|
15
|
+
export declare function getObjectSize(obj: any): number;
|
|
16
|
+
/**
|
|
17
|
+
* Validate event name
|
|
18
|
+
*/
|
|
19
|
+
export declare function isValidEventName(name: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Sanitize event properties
|
|
22
|
+
*/
|
|
23
|
+
export declare function sanitizeProperties(properties: Record<string, any>): Record<string, any>;
|
|
24
|
+
/**
|
|
25
|
+
* Create a logger that respects debug mode
|
|
26
|
+
*/
|
|
27
|
+
export declare function createLogger(debug: boolean): {
|
|
28
|
+
log: (...args: any[]) => void;
|
|
29
|
+
warn: (...args: any[]) => void;
|
|
30
|
+
error: (...args: any[]) => void;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Sleep for a given number of milliseconds
|
|
34
|
+
*/
|
|
35
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Calculate exponential backoff delay
|
|
38
|
+
*/
|
|
39
|
+
export declare function calculateBackoff(attempt: number, initialDelay: number, maxDelay: number, multiplier: number): number;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Utility functions for the SDK
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateUUID = generateUUID;
|
|
7
|
+
exports.safeStringify = safeStringify;
|
|
8
|
+
exports.getObjectSize = getObjectSize;
|
|
9
|
+
exports.isValidEventName = isValidEventName;
|
|
10
|
+
exports.sanitizeProperties = sanitizeProperties;
|
|
11
|
+
exports.createLogger = createLogger;
|
|
12
|
+
exports.sleep = sleep;
|
|
13
|
+
exports.calculateBackoff = calculateBackoff;
|
|
14
|
+
/**
|
|
15
|
+
* Generate a UUID v4
|
|
16
|
+
*/
|
|
17
|
+
function generateUUID() {
|
|
18
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
19
|
+
const r = (Math.random() * 16) | 0;
|
|
20
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
21
|
+
return v.toString(16);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Safely stringify an object, handling circular references
|
|
26
|
+
*/
|
|
27
|
+
function safeStringify(obj) {
|
|
28
|
+
const seen = new WeakSet();
|
|
29
|
+
return JSON.stringify(obj, (key, value) => {
|
|
30
|
+
if (typeof value === 'object' && value !== null) {
|
|
31
|
+
if (seen.has(value)) {
|
|
32
|
+
return '[Circular]';
|
|
33
|
+
}
|
|
34
|
+
seen.add(value);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Calculate size of an object in bytes
|
|
41
|
+
*/
|
|
42
|
+
function getObjectSize(obj) {
|
|
43
|
+
return new Blob([safeStringify(obj)]).size;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate event name
|
|
47
|
+
*/
|
|
48
|
+
function isValidEventName(name) {
|
|
49
|
+
if (!name || typeof name !== 'string')
|
|
50
|
+
return false;
|
|
51
|
+
if (name.length === 0 || name.length > 255)
|
|
52
|
+
return false;
|
|
53
|
+
// Allow alphanumeric, spaces, underscores, hyphens
|
|
54
|
+
return /^[a-zA-Z0-9\s_-]+$/.test(name);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Sanitize event properties
|
|
58
|
+
*/
|
|
59
|
+
function sanitizeProperties(properties) {
|
|
60
|
+
const sanitized = {};
|
|
61
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
62
|
+
// Skip invalid keys
|
|
63
|
+
if (!key || typeof key !== 'string' || key.length > 255)
|
|
64
|
+
continue;
|
|
65
|
+
// Only allow primitive types
|
|
66
|
+
if (typeof value === 'string' ||
|
|
67
|
+
typeof value === 'number' ||
|
|
68
|
+
typeof value === 'boolean' ||
|
|
69
|
+
value === null ||
|
|
70
|
+
value === undefined) {
|
|
71
|
+
sanitized[key] = value;
|
|
72
|
+
}
|
|
73
|
+
else if (typeof value === 'object') {
|
|
74
|
+
// Convert objects/arrays to JSON strings
|
|
75
|
+
try {
|
|
76
|
+
sanitized[key] = safeStringify(value);
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
// Skip if can't stringify
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return sanitized;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Create a logger that respects debug mode
|
|
87
|
+
*/
|
|
88
|
+
function createLogger(debug) {
|
|
89
|
+
return {
|
|
90
|
+
log: (...args) => {
|
|
91
|
+
if (debug)
|
|
92
|
+
console.log('[Axyle]', ...args);
|
|
93
|
+
},
|
|
94
|
+
warn: (...args) => {
|
|
95
|
+
if (debug)
|
|
96
|
+
console.warn('[Axyle]', ...args);
|
|
97
|
+
},
|
|
98
|
+
error: (...args) => {
|
|
99
|
+
if (debug)
|
|
100
|
+
console.error('[Axyle]', ...args);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Sleep for a given number of milliseconds
|
|
106
|
+
*/
|
|
107
|
+
function sleep(ms) {
|
|
108
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Calculate exponential backoff delay
|
|
112
|
+
*/
|
|
113
|
+
function calculateBackoff(attempt, initialDelay, maxDelay, multiplier) {
|
|
114
|
+
const delay = initialDelay * Math.pow(multiplier, attempt);
|
|
115
|
+
return Math.min(delay, maxDelay);
|
|
116
|
+
}
|