@grainql/analytics-web 1.7.2 → 2.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/README.md +71 -777
- package/dist/cjs/index.d.ts +35 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/react/GrainProvider.d.ts +11 -0
- package/dist/cjs/react/GrainProvider.d.ts.map +1 -0
- package/dist/cjs/react/GrainProvider.js +79 -0
- package/dist/cjs/react/GrainProvider.js.map +1 -0
- package/dist/cjs/react/context.d.ts +11 -0
- package/dist/cjs/react/context.d.ts.map +1 -0
- package/dist/cjs/react/context.js +43 -0
- package/dist/cjs/react/context.js.map +1 -0
- package/dist/cjs/react/hooks/useAllConfigs.d.ts +8 -0
- package/dist/cjs/react/hooks/useAllConfigs.d.ts.map +1 -0
- package/dist/cjs/react/hooks/useAllConfigs.js +112 -0
- package/dist/cjs/react/hooks/useAllConfigs.js.map +1 -0
- package/dist/cjs/react/hooks/useConfig.d.ts +9 -0
- package/dist/cjs/react/hooks/useConfig.d.ts.map +1 -0
- package/dist/cjs/react/hooks/useConfig.js +116 -0
- package/dist/cjs/react/hooks/useConfig.js.map +1 -0
- package/dist/cjs/react/hooks/useGrainAnalytics.d.ts +6 -0
- package/dist/cjs/react/hooks/useGrainAnalytics.d.ts.map +1 -0
- package/dist/cjs/react/hooks/useGrainAnalytics.js +50 -0
- package/dist/cjs/react/hooks/useGrainAnalytics.js.map +1 -0
- package/dist/cjs/react/hooks/useTrack.d.ts +9 -0
- package/dist/cjs/react/hooks/useTrack.d.ts.map +1 -0
- package/dist/cjs/react/hooks/useTrack.js +53 -0
- package/dist/cjs/react/hooks/useTrack.js.map +1 -0
- package/dist/cjs/react/index.d.ts +36 -0
- package/dist/cjs/react/index.d.ts.map +1 -0
- package/dist/cjs/react/index.js +45 -0
- package/dist/cjs/react/index.js.map +1 -0
- package/dist/cjs/react/types.d.ts +33 -0
- package/dist/cjs/react/types.d.ts.map +1 -0
- package/dist/cjs/react/types.js +6 -0
- package/dist/cjs/react/types.js.map +1 -0
- package/dist/esm/index.d.ts +35 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/react/GrainProvider.d.ts +11 -0
- package/dist/esm/react/GrainProvider.d.ts.map +1 -0
- package/dist/esm/react/GrainProvider.js +43 -0
- package/dist/esm/react/GrainProvider.js.map +1 -0
- package/dist/esm/react/context.d.ts +11 -0
- package/dist/esm/react/context.d.ts.map +1 -0
- package/dist/esm/react/context.js +7 -0
- package/dist/esm/react/context.js.map +1 -0
- package/dist/esm/react/hooks/useAllConfigs.d.ts +8 -0
- package/dist/esm/react/hooks/useAllConfigs.d.ts.map +1 -0
- package/dist/esm/react/hooks/useAllConfigs.js +76 -0
- package/dist/esm/react/hooks/useAllConfigs.js.map +1 -0
- package/dist/esm/react/hooks/useConfig.d.ts +9 -0
- package/dist/esm/react/hooks/useConfig.d.ts.map +1 -0
- package/dist/esm/react/hooks/useConfig.js +80 -0
- package/dist/esm/react/hooks/useConfig.js.map +1 -0
- package/dist/esm/react/hooks/useGrainAnalytics.d.ts +6 -0
- package/dist/esm/react/hooks/useGrainAnalytics.d.ts.map +1 -0
- package/dist/esm/react/hooks/useGrainAnalytics.js +14 -0
- package/dist/esm/react/hooks/useGrainAnalytics.js.map +1 -0
- package/dist/esm/react/hooks/useTrack.d.ts +9 -0
- package/dist/esm/react/hooks/useTrack.d.ts.map +1 -0
- package/dist/esm/react/hooks/useTrack.js +17 -0
- package/dist/esm/react/hooks/useTrack.js.map +1 -0
- package/dist/esm/react/index.d.ts +36 -0
- package/dist/esm/react/index.d.ts.map +1 -0
- package/dist/esm/react/index.js +37 -0
- package/dist/esm/react/index.js.map +1 -0
- package/dist/esm/react/types.d.ts +33 -0
- package/dist/esm/react/types.d.ts.map +1 -0
- package/dist/esm/react/types.js +5 -0
- package/dist/esm/react/types.js.map +1 -0
- package/dist/index.d.ts +35 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +124 -14
- package/dist/index.global.dev.js.map +2 -2
- package/dist/index.global.js +2 -2
- package/dist/index.global.js.map +3 -3
- package/dist/index.js +147 -15
- package/dist/index.mjs +147 -15
- package/dist/react/index.d.ts +405 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +1181 -0
- package/dist/react/index.mjs +1176 -0
- package/dist/react/react/GrainProvider.d.ts +11 -0
- package/dist/react/react/GrainProvider.d.ts.map +1 -0
- package/dist/react/react/GrainProvider.js +45 -0
- package/dist/react/react/GrainProvider.mjs +42 -0
- package/dist/react/react/context.d.ts +11 -0
- package/dist/react/react/context.d.ts.map +1 -0
- package/dist/react/react/context.js +9 -0
- package/dist/react/react/context.mjs +6 -0
- package/dist/react/react/hooks/useAllConfigs.d.ts +8 -0
- package/dist/react/react/hooks/useAllConfigs.d.ts.map +1 -0
- package/dist/react/react/hooks/useAllConfigs.js +78 -0
- package/dist/react/react/hooks/useAllConfigs.mjs +75 -0
- package/dist/react/react/hooks/useConfig.d.ts +9 -0
- package/dist/react/react/hooks/useConfig.d.ts.map +1 -0
- package/dist/react/react/hooks/useConfig.js +82 -0
- package/dist/react/react/hooks/useConfig.mjs +79 -0
- package/dist/react/react/hooks/useGrainAnalytics.d.ts +6 -0
- package/dist/react/react/hooks/useGrainAnalytics.d.ts.map +1 -0
- package/dist/react/react/hooks/useGrainAnalytics.js +16 -0
- package/dist/react/react/hooks/useGrainAnalytics.mjs +13 -0
- package/dist/react/react/hooks/useTrack.d.ts +9 -0
- package/dist/react/react/hooks/useTrack.d.ts.map +1 -0
- package/dist/react/react/hooks/useTrack.js +19 -0
- package/dist/react/react/hooks/useTrack.mjs +16 -0
- package/dist/react/react/index.d.ts +36 -0
- package/dist/react/react/index.d.ts.map +1 -0
- package/dist/react/react/index.js +44 -0
- package/dist/react/react/index.mjs +36 -0
- package/dist/react/react/types.d.ts +33 -0
- package/dist/react/react/types.d.ts.map +1 -0
- package/dist/react/react/types.js +5 -0
- package/dist/react/react/types.mjs +4 -0
- package/package.json +20 -2
package/README.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Grain Analytics Web SDK
|
|
2
2
|
|
|
3
|
-
A lightweight, dependency-free TypeScript SDK for
|
|
3
|
+
A lightweight, dependency-free TypeScript SDK for analytics and remote configuration management.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@grainql/analytics-web)
|
|
6
|
+
[](https://bundlephobia.com/package/@grainql/analytics-web)
|
|
4
7
|
|
|
5
8
|
## Features
|
|
6
9
|
|
|
7
|
-
- 🚀 **Zero dependencies** -
|
|
8
|
-
- 📦 **Automatic batching** - Efficient event
|
|
9
|
-
- 🔄 **Retry logic** -
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- 👤 **Global user tracking** - Set user ID once, used for all events
|
|
15
|
-
- 📊 **Template events** - Pre-built event tracking for common scenarios
|
|
16
|
-
- ⚙️ **Remote Config** - Dynamic configuration management with caching and real-time updates
|
|
10
|
+
- 🚀 **Zero dependencies** - ~6KB gzipped
|
|
11
|
+
- 📦 **Automatic batching** - Efficient event delivery
|
|
12
|
+
- 🔄 **Retry logic** - Reliable with exponential backoff
|
|
13
|
+
- 🎯 **TypeScript first** - Full type safety
|
|
14
|
+
- ⚙️ **Remote Config** - Dynamic app control without deployments
|
|
15
|
+
- ⚛️ **React Hooks** - Seamless React integration
|
|
16
|
+
- 📱 **Cross-platform** - Browser, Node.js, React Native
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
@@ -21,819 +21,113 @@ A lightweight, dependency-free TypeScript SDK for sending analytics events and m
|
|
|
21
21
|
npm install @grainql/analytics-web
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
> **Latest Version**: v1.6.0 includes comprehensive remote configuration management capabilities alongside the existing analytics features.
|
|
25
|
-
|
|
26
24
|
## Quick Start
|
|
27
25
|
|
|
28
|
-
###
|
|
26
|
+
### Vanilla JavaScript/TypeScript
|
|
29
27
|
|
|
30
28
|
```typescript
|
|
31
29
|
import { createGrainAnalytics } from '@grainql/analytics-web';
|
|
32
30
|
|
|
33
31
|
const grain = createGrainAnalytics({
|
|
34
|
-
tenantId: 'your-tenant-id'
|
|
35
|
-
authStrategy: 'NONE'
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// Track an event
|
|
39
|
-
grain.track('page_view', {
|
|
40
|
-
page: '/home',
|
|
41
|
-
referrer: document.referrer
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// Track with user ID
|
|
45
|
-
grain.track('button_click', {
|
|
46
|
-
button: 'signup',
|
|
47
|
-
userId: 'user123' // Note: Subject to same security restrictions as setProperty
|
|
48
|
-
});
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Global User ID
|
|
52
|
-
|
|
53
|
-
Set a user ID once and it will be used for all subsequent events:
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
// Set global user ID in config
|
|
57
|
-
const grain = createGrainAnalytics({
|
|
58
|
-
tenantId: 'your-tenant-id',
|
|
59
|
-
userId: 'user123' // Global user ID for all events
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Or set it after initialization
|
|
63
|
-
grain.setUserId('user123');
|
|
64
|
-
|
|
65
|
-
// All events will now use this user ID
|
|
66
|
-
grain.track('page_view', { page: '/dashboard' });
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### User Properties
|
|
70
|
-
|
|
71
|
-
Set user properties that can be used for analytics and segmentation:
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
// Set properties for the current user
|
|
75
|
-
await grain.setProperty({
|
|
76
|
-
plan: 'premium',
|
|
77
|
-
status: 'active',
|
|
78
|
-
signupDate: '2024-01-15',
|
|
79
|
-
source: 'web'
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// Set properties for a specific user
|
|
83
|
-
await grain.setProperty({
|
|
84
|
-
plan: 'free',
|
|
85
|
-
lastLogin: new Date().toISOString()
|
|
86
|
-
}, { userId: 'user123' });
|
|
87
|
-
|
|
88
|
-
// Properties are automatically serialized to strings
|
|
89
|
-
await grain.setProperty({
|
|
90
|
-
isActive: true, // Becomes "true"
|
|
91
|
-
count: 42, // Becomes "42"
|
|
92
|
-
metadata: { // Becomes JSON string
|
|
93
|
-
source: 'api',
|
|
94
|
-
version: '2.0'
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
**Important Security Notes:**
|
|
100
|
-
- You can set up to 4 properties per request, and all values are automatically converted to strings
|
|
101
|
-
- **UserId Override Restrictions**: When using `{ userId: 'specific-user' }` in options, ensure you have proper permissions:
|
|
102
|
-
- If your tenant requires JWT authentication, the userId must match the JWT subject
|
|
103
|
-
- Grain may block requests if too many distinct userIds are used from the same instance, browser, device, or IP
|
|
104
|
-
- Use userId overrides only when you have explicit permission to set properties for other users
|
|
105
|
-
|
|
106
|
-
### Server-Side Authentication
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
const grain = createGrainAnalytics({
|
|
110
|
-
tenantId: 'your-tenant-id',
|
|
111
|
-
authStrategy: 'SERVER_SIDE',
|
|
112
|
-
secretKey: 'your-secret-key'
|
|
113
|
-
});
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### JWT Authentication
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
const grain = createGrainAnalytics({
|
|
120
|
-
tenantId: 'your-tenant-id',
|
|
121
|
-
authStrategy: 'JWT',
|
|
122
|
-
authProvider: {
|
|
123
|
-
async getToken() {
|
|
124
|
-
// Return your JWT token from Auth0, next-auth, etc.
|
|
125
|
-
return await getAccessToken();
|
|
126
|
-
}
|
|
127
|
-
}
|
|
32
|
+
tenantId: 'your-tenant-id'
|
|
128
33
|
});
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Remote Config
|
|
132
34
|
|
|
133
|
-
|
|
35
|
+
// Track events
|
|
36
|
+
grain.track('page_viewed', { page: '/home' });
|
|
134
37
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
138
|
-
const grain = createGrainAnalytics({
|
|
139
|
-
tenantId: 'your-tenant-id',
|
|
140
|
-
userId: 'user123',
|
|
141
|
-
// Set default values for immediate access
|
|
142
|
-
defaultConfigurations: {
|
|
143
|
-
hero_text: 'Welcome to our app!',
|
|
144
|
-
button_color: 'blue',
|
|
145
|
-
feature_enabled: 'false'
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Get configuration value (synchronous, from cache or defaults)
|
|
150
|
-
const heroText = grain.getConfig('hero_text'); // Returns cached value or default
|
|
151
|
-
|
|
152
|
-
// Get configuration asynchronously (cache-first, with API fallback)
|
|
153
|
-
const buttonColor = await grain.getConfigAsync('button_color');
|
|
154
|
-
|
|
155
|
-
// Get all configurations
|
|
156
|
-
const allConfigs = grain.getAllConfigs();
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Preloading Configurations
|
|
160
|
-
|
|
161
|
-
Preload configurations at page load for immediate access:
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
// Set user ID first
|
|
165
|
-
grain.setUserId('user123');
|
|
166
|
-
|
|
167
|
-
// Preload specific keys for immediate access
|
|
168
|
-
await grain.preloadConfig(['hero_text', 'button_color', 'feature_enabled']);
|
|
169
|
-
|
|
170
|
-
// Now these values are available synchronously
|
|
38
|
+
// Get remote config
|
|
171
39
|
const heroText = grain.getConfig('hero_text');
|
|
172
|
-
const buttonColor = grain.getConfig('button_color');
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### Advanced Configuration Options
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
const grain = createGrainAnalytics({
|
|
179
|
-
tenantId: 'your-tenant-id',
|
|
180
|
-
userId: 'user123',
|
|
181
|
-
// Default values for immediate access
|
|
182
|
-
defaultConfigurations: {
|
|
183
|
-
hero_text: 'Default Hero Text',
|
|
184
|
-
button_color: 'blue',
|
|
185
|
-
feature_enabled: 'false'
|
|
186
|
-
},
|
|
187
|
-
// Custom cache key
|
|
188
|
-
configCacheKey: 'my_app_config',
|
|
189
|
-
// Auto-refresh every 2 minutes
|
|
190
|
-
configRefreshInterval: 120000,
|
|
191
|
-
// Enable/disable caching
|
|
192
|
-
enableConfigCache: true
|
|
193
|
-
});
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### Fetching Configurations with Properties
|
|
197
|
-
|
|
198
|
-
Send user properties to get personalized configurations:
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
// Fetch with user properties for personalization
|
|
202
|
-
const configs = await grain.getAllConfigsAsync({
|
|
203
|
-
properties: {
|
|
204
|
-
plan: 'premium',
|
|
205
|
-
location: 'US',
|
|
206
|
-
signup_date: '2024-01-15'
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// Or fetch specific keys with properties
|
|
211
|
-
const heroText = await grain.getConfigAsync('hero_text', {
|
|
212
|
-
properties: {
|
|
213
|
-
plan: 'premium',
|
|
214
|
-
location: 'US'
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
40
|
```
|
|
218
41
|
|
|
219
|
-
###
|
|
220
|
-
|
|
221
|
-
Listen for configuration changes in real-time:
|
|
42
|
+
### React
|
|
222
43
|
|
|
223
44
|
```typescript
|
|
224
|
-
|
|
225
|
-
grain.addConfigChangeListener((configurations) => {
|
|
226
|
-
console.log('Configurations updated:', configurations);
|
|
227
|
-
|
|
228
|
-
// Update UI based on new configurations
|
|
229
|
-
if (configurations.hero_text) {
|
|
230
|
-
document.getElementById('hero').textContent = configurations.hero_text;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (configurations.button_color) {
|
|
234
|
-
document.getElementById('cta-button').style.backgroundColor = configurations.button_color;
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
// Remove listener when no longer needed
|
|
239
|
-
const listener = (configs) => { /* ... */ };
|
|
240
|
-
grain.addConfigChangeListener(listener);
|
|
241
|
-
grain.removeConfigChangeListener(listener);
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
### Force Refresh
|
|
245
|
-
|
|
246
|
-
Force refresh configurations from the API:
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
// Force refresh a specific configuration
|
|
250
|
-
const latestHeroText = await grain.getConfigAsync('hero_text', {
|
|
251
|
-
forceRefresh: true
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
// Force refresh all configurations
|
|
255
|
-
const allLatestConfigs = await grain.getAllConfigsAsync({
|
|
256
|
-
forceRefresh: true
|
|
257
|
-
});
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
### Error Handling
|
|
261
|
-
|
|
262
|
-
The SDK gracefully handles configuration fetch failures:
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
try {
|
|
266
|
-
const configs = await grain.getAllConfigsAsync();
|
|
267
|
-
// Use configurations
|
|
268
|
-
} catch (error) {
|
|
269
|
-
console.error('Failed to fetch configurations:', error);
|
|
270
|
-
// Fall back to default values
|
|
271
|
-
const heroText = grain.getConfig('hero_text'); // Returns default if available
|
|
272
|
-
}
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
### React Integration Example
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
import { useEffect, useState } from 'react';
|
|
279
|
-
import { createGrainAnalytics } from '@grainql/analytics-web';
|
|
280
|
-
|
|
281
|
-
const grain = createGrainAnalytics({
|
|
282
|
-
tenantId: 'your-tenant-id',
|
|
283
|
-
userId: 'user123',
|
|
284
|
-
defaultConfigurations: {
|
|
285
|
-
hero_text: 'Loading...',
|
|
286
|
-
button_color: 'blue'
|
|
287
|
-
}
|
|
288
|
-
});
|
|
45
|
+
import { GrainProvider, useConfig, useTrack } from '@grainql/analytics-web/react';
|
|
289
46
|
|
|
290
47
|
function App() {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
// Listen for configuration changes
|
|
300
|
-
const handleConfigChange = (newConfigs) => {
|
|
301
|
-
setConfigs(newConfigs);
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
grain.addConfigChangeListener(handleConfigChange);
|
|
305
|
-
|
|
306
|
-
return () => {
|
|
307
|
-
grain.removeConfigChangeListener(handleConfigChange);
|
|
308
|
-
};
|
|
309
|
-
}, []);
|
|
48
|
+
return (
|
|
49
|
+
<GrainProvider config={{ tenantId: 'your-tenant-id' }}>
|
|
50
|
+
<HomePage />
|
|
51
|
+
</GrainProvider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
310
54
|
|
|
55
|
+
function HomePage() {
|
|
56
|
+
const { value: heroText } = useConfig('hero_text');
|
|
57
|
+
const track = useTrack();
|
|
58
|
+
|
|
311
59
|
return (
|
|
312
60
|
<div>
|
|
313
|
-
<h1
|
|
314
|
-
|
|
315
|
-
|
|
61
|
+
<h1>{heroText || 'Welcome!'}</h1>
|
|
62
|
+
<button onClick={() => track('cta_clicked')}>
|
|
63
|
+
Get Started
|
|
64
|
+
</button>
|
|
316
65
|
</div>
|
|
317
66
|
);
|
|
318
67
|
}
|
|
319
68
|
```
|
|
320
69
|
|
|
321
|
-
##
|
|
70
|
+
## Documentation
|
|
322
71
|
|
|
323
|
-
|
|
72
|
+
For comprehensive guides, API reference, and examples, visit our documentation:
|
|
324
73
|
|
|
325
|
-
|
|
74
|
+
**📚 [Full Documentation](https://docs.grainql.com)** <!-- Update with actual docs URL -->
|
|
326
75
|
|
|
327
|
-
|
|
328
|
-
// Track login
|
|
329
|
-
await grain.trackLogin({
|
|
330
|
-
method: 'email',
|
|
331
|
-
success: true,
|
|
332
|
-
rememberMe: true
|
|
333
|
-
});
|
|
76
|
+
### Key Topics
|
|
334
77
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
});
|
|
342
|
-
```
|
|
78
|
+
- **[Quick Start Guide](https://docs.grainql.com/quickstart)** - Get started in 5 minutes
|
|
79
|
+
- **[Event Tracking](https://docs.grainql.com/core/event-tracking)** - Track user actions
|
|
80
|
+
- **[Remote Configuration](https://docs.grainql.com/core/remote-config)** - Dynamic app control
|
|
81
|
+
- **[React Hooks](https://docs.grainql.com/react/overview)** - React integration
|
|
82
|
+
- **[API Reference](https://docs.grainql.com/api-reference/overview)** - Complete API docs
|
|
83
|
+
- **[Examples](https://docs.grainql.com/examples/react)** - Real-world examples
|
|
343
84
|
|
|
344
|
-
### E-commerce
|
|
345
85
|
|
|
346
|
-
|
|
347
|
-
// Track checkout
|
|
348
|
-
await grain.trackCheckout({
|
|
349
|
-
orderId: 'order_123',
|
|
350
|
-
total: 99.99,
|
|
351
|
-
currency: 'USD',
|
|
352
|
-
items: [
|
|
353
|
-
{ id: 'prod_1', name: 'Product 1', price: 49.99, quantity: 1 },
|
|
354
|
-
{ id: 'prod_2', name: 'Product 2', price: 50.00, quantity: 1 }
|
|
355
|
-
],
|
|
356
|
-
paymentMethod: 'credit_card',
|
|
357
|
-
success: true
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// Track purchase
|
|
361
|
-
await grain.trackPurchase({
|
|
362
|
-
orderId: 'order_123',
|
|
363
|
-
total: 99.99,
|
|
364
|
-
currency: 'USD',
|
|
365
|
-
paymentMethod: 'credit_card',
|
|
366
|
-
shipping: 5.99,
|
|
367
|
-
tax: 8.50
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// Track cart interactions
|
|
371
|
-
await grain.trackAddToCart({
|
|
372
|
-
itemId: 'prod_1',
|
|
373
|
-
itemName: 'Product 1',
|
|
374
|
-
price: 49.99,
|
|
375
|
-
quantity: 1,
|
|
376
|
-
currency: 'USD'
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
await grain.trackRemoveFromCart({
|
|
380
|
-
itemId: 'prod_2',
|
|
381
|
-
itemName: 'Product 2',
|
|
382
|
-
price: 50.00,
|
|
383
|
-
quantity: 1
|
|
384
|
-
});
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
### Page Views and Search
|
|
86
|
+
## Key Concepts
|
|
388
87
|
|
|
88
|
+
### Event Tracking
|
|
89
|
+
Track user actions with automatic batching and retry logic:
|
|
389
90
|
```typescript
|
|
390
|
-
|
|
391
|
-
await grain.
|
|
392
|
-
page: '/products',
|
|
393
|
-
title: 'Product Catalog',
|
|
394
|
-
referrer: 'https://google.com',
|
|
395
|
-
url: 'https://yoursite.com/products'
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// Track search
|
|
399
|
-
await grain.trackSearch({
|
|
400
|
-
query: 'blue shoes',
|
|
401
|
-
results: 24,
|
|
402
|
-
filters: { category: 'footwear', color: 'blue' },
|
|
403
|
-
sortBy: 'price_asc'
|
|
404
|
-
});
|
|
91
|
+
grain.track('button_clicked', { button: 'signup' });
|
|
92
|
+
await grain.trackPurchase({ orderId: '123', total: 99.99 });
|
|
405
93
|
```
|
|
406
94
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
### Configuration Options
|
|
410
|
-
|
|
95
|
+
### Remote Configuration
|
|
96
|
+
Control your app dynamically without code deployments:
|
|
411
97
|
```typescript
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
authStrategy?: AuthStrategy; // 'NONE' | 'SERVER_SIDE' | 'JWT' (default: 'NONE')
|
|
416
|
-
secretKey?: string; // Required for SERVER_SIDE auth
|
|
417
|
-
authProvider?: AuthProvider; // Required for JWT auth
|
|
418
|
-
userId?: string; // Global user ID for all events
|
|
419
|
-
batchSize?: number; // Events per batch (default: 50)
|
|
420
|
-
flushInterval?: number; // Auto-flush interval in ms (default: 5000)
|
|
421
|
-
retryAttempts?: number; // Retry attempts for failed requests (default: 3)
|
|
422
|
-
retryDelay?: number; // Base retry delay in ms (default: 1000)
|
|
423
|
-
debug?: boolean; // Enable debug logging (default: false)
|
|
424
|
-
// Remote Config options
|
|
425
|
-
defaultConfigurations?: Record<string, string>; // Default values for configurations
|
|
426
|
-
configCacheKey?: string; // Custom cache key for configurations (default: 'grain_config')
|
|
427
|
-
configRefreshInterval?: number; // Auto-refresh interval in ms (default: 300000)
|
|
428
|
-
enableConfigCache?: boolean; // Enable/disable configuration caching (default: true)
|
|
98
|
+
const featureEnabled = grain.getConfig('new_feature');
|
|
99
|
+
if (featureEnabled === 'true') {
|
|
100
|
+
// Show feature
|
|
429
101
|
}
|
|
430
102
|
```
|
|
431
103
|
|
|
432
|
-
###
|
|
433
|
-
|
|
434
|
-
#### `track(eventName, properties?, options?)`
|
|
435
|
-
|
|
436
|
-
Track a single event:
|
|
437
|
-
|
|
438
|
-
```typescript
|
|
439
|
-
grain.track('purchase', {
|
|
440
|
-
product_id: 'abc123',
|
|
441
|
-
price: 29.99,
|
|
442
|
-
currency: 'USD'
|
|
443
|
-
});
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
#### `track(event, options?)`
|
|
447
|
-
|
|
448
|
-
Track with a full event object:
|
|
449
|
-
|
|
104
|
+
### User Identification
|
|
105
|
+
Track users across sessions:
|
|
450
106
|
```typescript
|
|
451
|
-
grain.
|
|
452
|
-
|
|
453
|
-
userId: 'user123',
|
|
454
|
-
properties: {
|
|
455
|
-
product_id: 'abc123',
|
|
456
|
-
price: 29.99
|
|
457
|
-
},
|
|
458
|
-
timestamp: new Date()
|
|
459
|
-
});
|
|
107
|
+
grain.setUserId('user_123');
|
|
108
|
+
await grain.setProperty({ plan: 'premium' });
|
|
460
109
|
```
|
|
461
110
|
|
|
462
|
-
|
|
111
|
+
## More Examples
|
|
463
112
|
|
|
464
|
-
|
|
113
|
+
Check the [examples directory](./examples) for:
|
|
114
|
+
- Vanilla JavaScript usage
|
|
115
|
+
- React integration
|
|
116
|
+
- Next.js setup
|
|
117
|
+
- E-commerce tracking
|
|
118
|
+
- Authentication flows
|
|
465
119
|
|
|
466
|
-
|
|
467
|
-
grain.setUserId('user123');
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
#### `getUserId()`
|
|
471
|
-
|
|
472
|
-
Get current global user ID:
|
|
473
|
-
|
|
474
|
-
```typescript
|
|
475
|
-
const userId = grain.getUserId(); // Returns 'user123' or null
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
#### `identify(userId)`
|
|
479
|
-
|
|
480
|
-
Alias for `setUserId()` - sets user ID for subsequent events:
|
|
481
|
-
|
|
482
|
-
```typescript
|
|
483
|
-
grain.identify('user123');
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
#### `setProperty(properties, options?)`
|
|
487
|
-
|
|
488
|
-
Set user properties for analytics and segmentation:
|
|
489
|
-
|
|
490
|
-
```typescript
|
|
491
|
-
// Set properties for current user
|
|
492
|
-
await grain.setProperty({
|
|
493
|
-
plan: 'premium',
|
|
494
|
-
status: 'active',
|
|
495
|
-
signupDate: '2024-01-15'
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
// Set properties for specific user
|
|
499
|
-
await grain.setProperty({
|
|
500
|
-
plan: 'free'
|
|
501
|
-
}, { userId: 'user123' });
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
**Parameters:**
|
|
505
|
-
- `properties`: Object with up to 4 key-value pairs
|
|
506
|
-
- `options.userId`: Optional user ID override
|
|
507
|
-
|
|
508
|
-
**Security Considerations:**
|
|
509
|
-
- When using `options.userId`, ensure you have proper permissions to set properties for other users
|
|
510
|
-
- With JWT authentication, the userId must match the JWT subject
|
|
511
|
-
- Grain may block requests if too many distinct userIds are used from the same source
|
|
512
|
-
|
|
513
|
-
#### `flush()`
|
|
514
|
-
|
|
515
|
-
Manually flush all queued events:
|
|
516
|
-
|
|
517
|
-
```typescript
|
|
518
|
-
await grain.flush();
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
#### `destroy()`
|
|
522
|
-
|
|
523
|
-
Clean up resources and send remaining events:
|
|
524
|
-
|
|
525
|
-
```typescript
|
|
526
|
-
grain.destroy();
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
### Remote Config Methods
|
|
530
|
-
|
|
531
|
-
#### `getConfig(key)`
|
|
532
|
-
|
|
533
|
-
Get configuration value synchronously (from cache or defaults):
|
|
534
|
-
|
|
535
|
-
```typescript
|
|
536
|
-
const heroText = grain.getConfig('hero_text'); // Returns string | undefined
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
#### `getAllConfigs()`
|
|
540
|
-
|
|
541
|
-
Get all configurations synchronously (from cache or defaults):
|
|
542
|
-
|
|
543
|
-
```typescript
|
|
544
|
-
const configs = grain.getAllConfigs(); // Returns Record<string, string>
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
#### `getConfigAsync(key, options?)`
|
|
548
|
-
|
|
549
|
-
Get configuration value asynchronously (cache-first with API fallback):
|
|
550
|
-
|
|
551
|
-
```typescript
|
|
552
|
-
const heroText = await grain.getConfigAsync('hero_text');
|
|
553
|
-
const buttonColor = await grain.getConfigAsync('button_color', {
|
|
554
|
-
properties: { plan: 'premium' },
|
|
555
|
-
forceRefresh: true
|
|
556
|
-
});
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
#### `getAllConfigsAsync(options?)`
|
|
560
|
-
|
|
561
|
-
Get all configurations asynchronously (cache-first with API fallback):
|
|
562
|
-
|
|
563
|
-
```typescript
|
|
564
|
-
const configs = await grain.getAllConfigsAsync();
|
|
565
|
-
const allConfigs = await grain.getAllConfigsAsync({
|
|
566
|
-
properties: { plan: 'premium', location: 'US' },
|
|
567
|
-
forceRefresh: true
|
|
568
|
-
});
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
#### `fetchConfig(options?)`
|
|
572
|
-
|
|
573
|
-
Fetch configurations directly from API:
|
|
574
|
-
|
|
575
|
-
```typescript
|
|
576
|
-
const response = await grain.fetchConfig({
|
|
577
|
-
immediateKeys: ['hero_text', 'button_color'],
|
|
578
|
-
properties: { plan: 'premium' }
|
|
579
|
-
});
|
|
580
|
-
// Returns RemoteConfigResponse with full API response
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
#### `preloadConfig(immediateKeys?, properties?)`
|
|
584
|
-
|
|
585
|
-
Preload configurations for immediate access:
|
|
586
|
-
|
|
587
|
-
```typescript
|
|
588
|
-
await grain.preloadConfig(['hero_text', 'button_color']);
|
|
589
|
-
// Configurations are now available synchronously
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
#### `addConfigChangeListener(listener)`
|
|
593
|
-
|
|
594
|
-
Add listener for configuration changes:
|
|
595
|
-
|
|
596
|
-
```typescript
|
|
597
|
-
const listener = (configurations) => {
|
|
598
|
-
console.log('Configs updated:', configurations);
|
|
599
|
-
};
|
|
600
|
-
grain.addConfigChangeListener(listener);
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
#### `removeConfigChangeListener(listener)`
|
|
604
|
-
|
|
605
|
-
Remove configuration change listener:
|
|
606
|
-
|
|
607
|
-
```typescript
|
|
608
|
-
grain.removeConfigChangeListener(listener);
|
|
609
|
-
```
|
|
610
|
-
|
|
611
|
-
### Template Event Methods
|
|
612
|
-
|
|
613
|
-
#### Authentication Events
|
|
614
|
-
|
|
615
|
-
- `trackLogin(properties?, options?)` - Track user login
|
|
616
|
-
- `trackSignup(properties?, options?)` - Track user signup
|
|
617
|
-
|
|
618
|
-
#### E-commerce Events
|
|
619
|
-
|
|
620
|
-
- `trackCheckout(properties?, options?)` - Track checkout process
|
|
621
|
-
- `trackPurchase(properties?, options?)` - Track completed purchase
|
|
622
|
-
- `trackAddToCart(properties?, options?)` - Track item added to cart
|
|
623
|
-
- `trackRemoveFromCart(properties?, options?)` - Track item removed from cart
|
|
624
|
-
|
|
625
|
-
#### Navigation Events
|
|
626
|
-
|
|
627
|
-
- `trackPageView(properties?, options?)` - Track page views
|
|
628
|
-
- `trackSearch(properties?, options?)` - Track search queries
|
|
629
|
-
|
|
630
|
-
### Template Event Properties
|
|
631
|
-
|
|
632
|
-
All template events support their own typed properties:
|
|
633
|
-
|
|
634
|
-
```typescript
|
|
635
|
-
// Login event properties
|
|
636
|
-
interface LoginEventProperties {
|
|
637
|
-
method?: string; // 'email', 'google', 'facebook', etc.
|
|
638
|
-
success?: boolean; // Whether login was successful
|
|
639
|
-
errorMessage?: string; // Error message if login failed
|
|
640
|
-
loginAttempt?: number; // Attempt number
|
|
641
|
-
rememberMe?: boolean; // Whether "remember me" was checked
|
|
642
|
-
twoFactorEnabled?: boolean; // Whether 2FA was used
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Checkout event properties
|
|
646
|
-
interface CheckoutEventProperties {
|
|
647
|
-
orderId?: string; // Unique order identifier
|
|
648
|
-
total?: number; // Total amount
|
|
649
|
-
currency?: string; // Currency code
|
|
650
|
-
items?: Array<{ // Array of items
|
|
651
|
-
id: string;
|
|
652
|
-
name: string;
|
|
653
|
-
price: number;
|
|
654
|
-
quantity: number;
|
|
655
|
-
}>;
|
|
656
|
-
paymentMethod?: string; // 'credit_card', 'paypal', 'stripe', etc.
|
|
657
|
-
success?: boolean; // Whether checkout was successful
|
|
658
|
-
errorMessage?: string; // Error message if checkout failed
|
|
659
|
-
couponCode?: string; // Applied coupon code
|
|
660
|
-
discount?: number; // Discount amount
|
|
661
|
-
}
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
## Security Considerations
|
|
665
|
-
|
|
666
|
-
### User ID Override Restrictions
|
|
667
|
-
|
|
668
|
-
When using userId overrides in event tracking or property setting, be aware of these security restrictions:
|
|
669
|
-
|
|
670
|
-
- **JWT Authentication**: If your tenant requires JWT authentication, any userId override must match the JWT subject
|
|
671
|
-
- **Rate Limiting**: Grain may block requests if too many distinct userIds are used from the same instance, browser, device, or IP address
|
|
672
|
-
- **Permissions**: Only use userId overrides when you have explicit permission to track events or set properties for other users
|
|
673
|
-
|
|
674
|
-
**Best Practices:**
|
|
675
|
-
- Use global `setUserId()` for the current user instead of per-event userId overrides
|
|
676
|
-
- Avoid switching between many different userIds from the same client
|
|
677
|
-
- Ensure your authentication strategy aligns with your userId usage patterns
|
|
678
|
-
|
|
679
|
-
## Authentication Strategies
|
|
680
|
-
|
|
681
|
-
### NONE
|
|
682
|
-
No authentication required. Events are sent directly to the API.
|
|
683
|
-
|
|
684
|
-
### SERVER_SIDE
|
|
685
|
-
Use a secret key for server-side authentication:
|
|
686
|
-
- Obtain your secret key from the Grain dashboard
|
|
687
|
-
- Include it in your configuration
|
|
688
|
-
- Events are sent with `Authorization: Chase {SECRET}` header
|
|
689
|
-
|
|
690
|
-
### JWT
|
|
691
|
-
Use JWT tokens for client-side authentication:
|
|
692
|
-
- Configure JWT settings in your Grain tenant
|
|
693
|
-
- Provide an auth provider that returns valid tokens
|
|
694
|
-
- Supports Auth0, next-auth, and other JWT providers
|
|
695
|
-
|
|
696
|
-
## Build Outputs
|
|
697
|
-
|
|
698
|
-
The package provides multiple build formats:
|
|
699
|
-
|
|
700
|
-
- **ESM**: `dist/index.mjs` - Modern ES modules
|
|
701
|
-
- **CommonJS**: `dist/index.js` - Node.js compatible
|
|
702
|
-
- **IIFE**: `dist/index.global.js` - Browser global (`window.Grain`)
|
|
703
|
-
- **Types**: `dist/index.d.ts` - TypeScript definitions
|
|
704
|
-
|
|
705
|
-
### Browser Usage (Script Tag)
|
|
706
|
-
|
|
707
|
-
```html
|
|
708
|
-
<script src="https://unpkg.com/@grainql/analytics-web/dist/index.global.js"></script>
|
|
709
|
-
<script>
|
|
710
|
-
const grain = Grain.createGrainAnalytics({
|
|
711
|
-
tenantId: 'your-tenant-id',
|
|
712
|
-
userId: 'user123'
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
grain.trackPageView({ page: '/home' });
|
|
716
|
-
grain.trackLogin({ method: 'email', success: true });
|
|
717
|
-
</script>
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
## Advanced Usage
|
|
721
|
-
|
|
722
|
-
### Custom Auth Provider
|
|
723
|
-
|
|
724
|
-
```typescript
|
|
725
|
-
class Auth0Provider {
|
|
726
|
-
constructor(private auth0Client) {}
|
|
727
|
-
|
|
728
|
-
async getToken() {
|
|
729
|
-
return await this.auth0Client.getTokenSilently();
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
const grain = createGrainAnalytics({
|
|
734
|
-
tenantId: 'your-tenant-id',
|
|
735
|
-
authStrategy: 'JWT',
|
|
736
|
-
authProvider: new Auth0Provider(auth0Client)
|
|
737
|
-
});
|
|
738
|
-
```
|
|
739
|
-
|
|
740
|
-
### Error Handling
|
|
741
|
-
|
|
742
|
-
```typescript
|
|
743
|
-
try {
|
|
744
|
-
await grain.track('event', { data: 'value' }, { flush: true });
|
|
745
|
-
} catch (error) {
|
|
746
|
-
console.error('Failed to send event:', error);
|
|
747
|
-
}
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
### Debug Mode
|
|
751
|
-
|
|
752
|
-
```typescript
|
|
753
|
-
const grain = createGrainAnalytics({
|
|
754
|
-
tenantId: 'your-tenant-id',
|
|
755
|
-
debug: true // Enables console logging
|
|
756
|
-
});
|
|
757
|
-
```
|
|
758
|
-
|
|
759
|
-
### User Session Management
|
|
760
|
-
|
|
761
|
-
```typescript
|
|
762
|
-
// Set user ID when user logs in
|
|
763
|
-
grain.setUserId('user123');
|
|
764
|
-
|
|
765
|
-
// Track user-specific events
|
|
766
|
-
await grain.trackLogin({ method: 'email', success: true });
|
|
767
|
-
await grain.trackPageView({ page: '/dashboard' });
|
|
768
|
-
|
|
769
|
-
// Clear user ID when user logs out
|
|
770
|
-
grain.setUserId(null);
|
|
771
|
-
```
|
|
120
|
+
## Contributing
|
|
772
121
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
### [1.6.0] - 2025-09-08
|
|
776
|
-
|
|
777
|
-
#### Added
|
|
778
|
-
- **Remote Config API**: Complete remote configuration management system
|
|
779
|
-
- `getConfig()` and `getAllConfigs()` for synchronous access
|
|
780
|
-
- `getConfigAsync()` and `getAllConfigsAsync()` for async access with cache-first strategy
|
|
781
|
-
- `fetchConfig()` for direct API calls
|
|
782
|
-
- `preloadConfig()` for preloading configurations at page load
|
|
783
|
-
- Configuration change listeners with `addConfigChangeListener()` and `removeConfigChangeListener()`
|
|
784
|
-
- Automatic configuration caching with localStorage persistence
|
|
785
|
-
- Auto-refresh timer for keeping configurations up-to-date
|
|
786
|
-
- Default values support for immediate access without API calls
|
|
787
|
-
- User properties support for personalized configurations
|
|
788
|
-
- Force refresh option to bypass cache
|
|
789
|
-
- Full authentication support (NONE, SERVER_SIDE, JWT)
|
|
790
|
-
- **Enhanced Configuration Options**:
|
|
791
|
-
- `defaultConfigurations` for setting default values
|
|
792
|
-
- `configCacheKey` for custom cache keys
|
|
793
|
-
- `configRefreshInterval` for auto-refresh timing
|
|
794
|
-
- `enableConfigCache` for cache control
|
|
795
|
-
|
|
796
|
-
#### Technical
|
|
797
|
-
- Added comprehensive remote config interfaces and types
|
|
798
|
-
- Implemented robust error handling and retry logic for config API
|
|
799
|
-
- Added localStorage-based caching with graceful fallbacks
|
|
800
|
-
- Integrated config refresh timer with proper cleanup
|
|
801
|
-
- Enhanced TypeScript interfaces for better type safety
|
|
802
|
-
- Added comprehensive test coverage for remote config functionality
|
|
803
|
-
- Improved test stability and reliability
|
|
804
|
-
|
|
805
|
-
### [1.4.0] - 2024-12-19
|
|
806
|
-
|
|
807
|
-
#### Added
|
|
808
|
-
- **User Properties API**: New `setProperty()` method for setting user properties
|
|
809
|
-
- Set up to 4 properties per request
|
|
810
|
-
- Automatic string serialization for all values
|
|
811
|
-
- Support for user-specific property overrides
|
|
812
|
-
- Full authentication support (NONE, SERVER_SIDE, JWT)
|
|
813
|
-
- **Updated API Endpoints**:
|
|
814
|
-
- Events now use `/v1/events/{tenant}/multi` endpoint
|
|
815
|
-
- Properties use `/v1/events/{tenant}/properties` endpoint
|
|
816
|
-
|
|
817
|
-
#### Changed
|
|
818
|
-
- Updated all event sending to use the new `/multi` endpoint
|
|
819
|
-
- Enhanced error handling and retry logic for properties API
|
|
820
|
-
|
|
821
|
-
#### Technical
|
|
822
|
-
- Added comprehensive test coverage for properties functionality
|
|
823
|
-
- Updated all existing tests to use new endpoint structure
|
|
824
|
-
- Improved TypeScript interfaces for better type safety
|
|
825
|
-
|
|
826
|
-
### [1.3.0] - Previous Release
|
|
827
|
-
|
|
828
|
-
- Template events for common analytics scenarios
|
|
829
|
-
- Enhanced user ID management
|
|
830
|
-
- Improved error handling and retry logic
|
|
831
|
-
- Cross-platform compatibility improvements
|
|
122
|
+
We welcome contributions! Please see our contributing guidelines for more details.
|
|
832
123
|
|
|
833
124
|
## License
|
|
834
125
|
|
|
835
|
-
MIT
|
|
126
|
+
MIT © Grain Analytics
|
|
836
127
|
|
|
837
128
|
## Support
|
|
838
129
|
|
|
839
|
-
|
|
130
|
+
- **Documentation**: [docs.grainql.com](https://docs.grainql.com)
|
|
131
|
+
- **Dashboard**: [grainql.com/dashboard](https://grainql.com/dashboard)
|
|
132
|
+
- **Issues**: [GitHub Issues](https://github.com/GrainQL/analytics-web/issues)
|
|
133
|
+
- **Email**: support@grainql.com
|