@grainql/analytics-web 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +47 -0
  2. package/dist/cjs/consent.d.ts +4 -0
  3. package/dist/cjs/consent.d.ts.map +1 -1
  4. package/dist/cjs/consent.js +5 -0
  5. package/dist/cjs/consent.js.map +1 -1
  6. package/dist/cjs/heartbeat.d.ts +5 -0
  7. package/dist/cjs/heartbeat.d.ts.map +1 -1
  8. package/dist/cjs/heartbeat.js +35 -7
  9. package/dist/cjs/heartbeat.js.map +1 -1
  10. package/dist/cjs/index.d.ts +20 -0
  11. package/dist/cjs/index.d.ts.map +1 -1
  12. package/dist/cjs/index.js.map +1 -1
  13. package/dist/consent.d.ts +4 -0
  14. package/dist/consent.d.ts.map +1 -1
  15. package/dist/consent.js +5 -0
  16. package/dist/esm/consent.d.ts +4 -0
  17. package/dist/esm/consent.d.ts.map +1 -1
  18. package/dist/esm/consent.js +5 -0
  19. package/dist/esm/consent.js.map +1 -1
  20. package/dist/esm/heartbeat.d.ts +5 -0
  21. package/dist/esm/heartbeat.d.ts.map +1 -1
  22. package/dist/esm/heartbeat.js +35 -7
  23. package/dist/esm/heartbeat.js.map +1 -1
  24. package/dist/esm/index.d.ts +20 -0
  25. package/dist/esm/index.d.ts.map +1 -1
  26. package/dist/esm/index.js.map +1 -1
  27. package/dist/heartbeat.d.ts +5 -0
  28. package/dist/heartbeat.d.ts.map +1 -1
  29. package/dist/heartbeat.js +35 -7
  30. package/dist/index.d.ts +20 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.global.dev.js +78 -23
  33. package/dist/index.global.dev.js.map +2 -2
  34. package/dist/index.global.js +2 -2
  35. package/dist/index.global.js.map +3 -3
  36. package/dist/index.js +53 -23
  37. package/dist/index.mjs +53 -23
  38. package/dist/react/consent.d.ts +4 -0
  39. package/dist/react/consent.d.ts.map +1 -1
  40. package/dist/react/consent.js +5 -0
  41. package/dist/react/consent.mjs +5 -0
  42. package/dist/react/heartbeat.d.ts +5 -0
  43. package/dist/react/heartbeat.d.ts.map +1 -1
  44. package/dist/react/heartbeat.js +35 -7
  45. package/dist/react/heartbeat.mjs +35 -7
  46. package/dist/react/index.d.ts +20 -0
  47. package/dist/react/index.d.ts.map +1 -1
  48. package/dist/react/index.js +53 -23
  49. package/dist/react/index.mjs +53 -23
  50. package/package.json +5 -2
@@ -116,6 +116,23 @@ class GrainAnalytics {
116
116
  return v.toString(16);
117
117
  });
118
118
  }
119
+ /**
120
+ * Check if we should allow persistent storage (GDPR compliance)
121
+ *
122
+ * Returns true if:
123
+ * - Consent has been granted, OR
124
+ * - Not in opt-in mode, OR
125
+ * - User has been explicitly identified by the site (login/identify), OR
126
+ * - Using JWT auth strategy (functional/essential purpose)
127
+ */
128
+ shouldAllowPersistentStorage() {
129
+ const hasConsent = this.consentManager.hasConsent('analytics');
130
+ const isOptInMode = this.config.consentMode === 'opt-in';
131
+ const userExplicitlyIdentified = !!this.globalUserId;
132
+ const isJWTAuth = this.config.authStrategy === 'JWT';
133
+ // Allow persistent storage if any of these conditions are met
134
+ return hasConsent || !isOptInMode || userExplicitlyIdentified || isJWTAuth;
135
+ }
119
136
  /**
120
137
  * Generate a proper UUIDv4 identifier for anonymous user ID
121
138
  */
@@ -125,10 +142,19 @@ class GrainAnalytics {
125
142
  /**
126
143
  * Initialize persistent anonymous user ID from cookies or localStorage
127
144
  * Priority: Cookie → localStorage → generate new
145
+ *
146
+ * GDPR Compliance: In opt-in mode without consent, we skip loading/saving
147
+ * persistent IDs unless the user has been explicitly identified by the site
148
+ * or when using JWT auth (functional/essential purpose)
128
149
  */
129
150
  initializePersistentAnonymousUserId() {
130
151
  if (typeof window === 'undefined')
131
152
  return;
153
+ // Check if we should avoid persistent storage (GDPR compliance)
154
+ if (!this.shouldAllowPersistentStorage()) {
155
+ this.log('Opt-in mode without consent: skipping persistent ID initialization (GDPR compliance)');
156
+ return; // Don't load or create persistent ID
157
+ }
132
158
  const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
133
159
  const cookieName = '_grain_uid';
134
160
  try {
@@ -166,10 +192,18 @@ class GrainAnalytics {
166
192
  }
167
193
  /**
168
194
  * Save persistent anonymous user ID to cookie and/or localStorage
195
+ *
196
+ * GDPR Compliance: In opt-in mode without consent, we don't persist IDs
197
+ * unless the user has been explicitly identified or using JWT auth
169
198
  */
170
199
  savePersistentAnonymousUserId(userId) {
171
200
  if (typeof window === 'undefined')
172
201
  return;
202
+ // Check if we should avoid persistent storage (GDPR compliance)
203
+ if (!this.shouldAllowPersistentStorage()) {
204
+ this.log('Opt-in mode without consent: skipping persistent ID save (GDPR compliance)');
205
+ return; // Don't save persistent ID
206
+ }
173
207
  const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
174
208
  const cookieName = '_grain_uid';
175
209
  try {
@@ -178,7 +212,7 @@ class GrainAnalytics {
178
212
  const cookieOptions = {
179
213
  maxAge: 365 * 24 * 60 * 60, // 365 days
180
214
  sameSite: 'lax',
181
- secure: window.location.protocol === 'https:',
215
+ secure: typeof window !== 'undefined' && window.location.protocol === 'https:',
182
216
  ...this.config.cookieOptions,
183
217
  };
184
218
  (0, cookies_1.setCookie)(cookieName, userId, cookieOptions);
@@ -192,6 +226,9 @@ class GrainAnalytics {
192
226
  }
193
227
  /**
194
228
  * Get the effective user ID (global userId or persistent anonymous ID)
229
+ *
230
+ * GDPR Compliance: In opt-in mode without consent and no explicit user identification,
231
+ * this should not be called. Use getEphemeralSessionId() instead.
195
232
  */
196
233
  getEffectiveUserIdInternal() {
197
234
  if (this.globalUserId) {
@@ -202,16 +239,8 @@ class GrainAnalytics {
202
239
  }
203
240
  // Generate a new UUIDv4 identifier as fallback
204
241
  this.persistentAnonymousUserId = this.generateAnonymousUserId();
205
- // Try to persist it
206
- if (typeof window !== 'undefined') {
207
- try {
208
- const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
209
- localStorage.setItem(storageKey, this.persistentAnonymousUserId);
210
- }
211
- catch (error) {
212
- this.log('Failed to persist generated anonymous user ID:', error);
213
- }
214
- }
242
+ // Try to persist it (will be skipped in opt-in mode without consent)
243
+ this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
215
244
  return this.persistentAnonymousUserId;
216
245
  }
217
246
  log(...args) {
@@ -474,6 +503,8 @@ class GrainAnalytics {
474
503
  }
475
504
  }
476
505
  startFlushTimer() {
506
+ if (typeof window === 'undefined')
507
+ return;
477
508
  if (this.flushTimer) {
478
509
  clearInterval(this.flushTimer);
479
510
  }
@@ -557,7 +588,12 @@ class GrainAnalytics {
557
588
  */
558
589
  handleConsentGranted() {
559
590
  this.flushWaitingForConsentQueue();
560
- // Track consent granted event with mapping
591
+ // Initialize persistent ID now that consent is granted (if not already initialized)
592
+ if (!this.persistentAnonymousUserId) {
593
+ this.initializePersistentAnonymousUserId();
594
+ this.log('Initialized persistent ID after consent grant');
595
+ }
596
+ // Track consent granted event with mapping from ephemeral to persistent ID
561
597
  if (this.ephemeralSessionId) {
562
598
  this.trackSystemEvent('_grain_consent_granted', {
563
599
  previous_session_id: this.ephemeralSessionId,
@@ -727,19 +763,11 @@ class GrainAnalytics {
727
763
  this.persistentAnonymousUserId = null;
728
764
  }
729
765
  else {
730
- // If clearing user ID, ensure we have a UUIDv4 identifier
766
+ // If clearing user ID, ensure we have an identifier
731
767
  if (!this.persistentAnonymousUserId) {
732
768
  this.persistentAnonymousUserId = this.generateAnonymousUserId();
733
- // Try to persist the new anonymous ID
734
- if (typeof window !== 'undefined') {
735
- try {
736
- const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
737
- localStorage.setItem(storageKey, this.persistentAnonymousUserId);
738
- }
739
- catch (error) {
740
- this.log('Failed to persist new anonymous user ID:', error);
741
- }
742
- }
769
+ // Try to persist the new anonymous ID (respects GDPR opt-in mode)
770
+ this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
743
771
  }
744
772
  }
745
773
  }
@@ -1320,6 +1348,8 @@ class GrainAnalytics {
1320
1348
  * Start automatic configuration refresh timer
1321
1349
  */
1322
1350
  startConfigRefreshTimer() {
1351
+ if (typeof window === 'undefined')
1352
+ return;
1323
1353
  if (this.configRefreshTimer) {
1324
1354
  clearInterval(this.configRefreshTimer);
1325
1355
  }
@@ -112,6 +112,23 @@ export class GrainAnalytics {
112
112
  return v.toString(16);
113
113
  });
114
114
  }
115
+ /**
116
+ * Check if we should allow persistent storage (GDPR compliance)
117
+ *
118
+ * Returns true if:
119
+ * - Consent has been granted, OR
120
+ * - Not in opt-in mode, OR
121
+ * - User has been explicitly identified by the site (login/identify), OR
122
+ * - Using JWT auth strategy (functional/essential purpose)
123
+ */
124
+ shouldAllowPersistentStorage() {
125
+ const hasConsent = this.consentManager.hasConsent('analytics');
126
+ const isOptInMode = this.config.consentMode === 'opt-in';
127
+ const userExplicitlyIdentified = !!this.globalUserId;
128
+ const isJWTAuth = this.config.authStrategy === 'JWT';
129
+ // Allow persistent storage if any of these conditions are met
130
+ return hasConsent || !isOptInMode || userExplicitlyIdentified || isJWTAuth;
131
+ }
115
132
  /**
116
133
  * Generate a proper UUIDv4 identifier for anonymous user ID
117
134
  */
@@ -121,10 +138,19 @@ export class GrainAnalytics {
121
138
  /**
122
139
  * Initialize persistent anonymous user ID from cookies or localStorage
123
140
  * Priority: Cookie → localStorage → generate new
141
+ *
142
+ * GDPR Compliance: In opt-in mode without consent, we skip loading/saving
143
+ * persistent IDs unless the user has been explicitly identified by the site
144
+ * or when using JWT auth (functional/essential purpose)
124
145
  */
125
146
  initializePersistentAnonymousUserId() {
126
147
  if (typeof window === 'undefined')
127
148
  return;
149
+ // Check if we should avoid persistent storage (GDPR compliance)
150
+ if (!this.shouldAllowPersistentStorage()) {
151
+ this.log('Opt-in mode without consent: skipping persistent ID initialization (GDPR compliance)');
152
+ return; // Don't load or create persistent ID
153
+ }
128
154
  const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
129
155
  const cookieName = '_grain_uid';
130
156
  try {
@@ -162,10 +188,18 @@ export class GrainAnalytics {
162
188
  }
163
189
  /**
164
190
  * Save persistent anonymous user ID to cookie and/or localStorage
191
+ *
192
+ * GDPR Compliance: In opt-in mode without consent, we don't persist IDs
193
+ * unless the user has been explicitly identified or using JWT auth
165
194
  */
166
195
  savePersistentAnonymousUserId(userId) {
167
196
  if (typeof window === 'undefined')
168
197
  return;
198
+ // Check if we should avoid persistent storage (GDPR compliance)
199
+ if (!this.shouldAllowPersistentStorage()) {
200
+ this.log('Opt-in mode without consent: skipping persistent ID save (GDPR compliance)');
201
+ return; // Don't save persistent ID
202
+ }
169
203
  const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
170
204
  const cookieName = '_grain_uid';
171
205
  try {
@@ -174,7 +208,7 @@ export class GrainAnalytics {
174
208
  const cookieOptions = {
175
209
  maxAge: 365 * 24 * 60 * 60, // 365 days
176
210
  sameSite: 'lax',
177
- secure: window.location.protocol === 'https:',
211
+ secure: typeof window !== 'undefined' && window.location.protocol === 'https:',
178
212
  ...this.config.cookieOptions,
179
213
  };
180
214
  setCookie(cookieName, userId, cookieOptions);
@@ -188,6 +222,9 @@ export class GrainAnalytics {
188
222
  }
189
223
  /**
190
224
  * Get the effective user ID (global userId or persistent anonymous ID)
225
+ *
226
+ * GDPR Compliance: In opt-in mode without consent and no explicit user identification,
227
+ * this should not be called. Use getEphemeralSessionId() instead.
191
228
  */
192
229
  getEffectiveUserIdInternal() {
193
230
  if (this.globalUserId) {
@@ -198,16 +235,8 @@ export class GrainAnalytics {
198
235
  }
199
236
  // Generate a new UUIDv4 identifier as fallback
200
237
  this.persistentAnonymousUserId = this.generateAnonymousUserId();
201
- // Try to persist it
202
- if (typeof window !== 'undefined') {
203
- try {
204
- const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
205
- localStorage.setItem(storageKey, this.persistentAnonymousUserId);
206
- }
207
- catch (error) {
208
- this.log('Failed to persist generated anonymous user ID:', error);
209
- }
210
- }
238
+ // Try to persist it (will be skipped in opt-in mode without consent)
239
+ this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
211
240
  return this.persistentAnonymousUserId;
212
241
  }
213
242
  log(...args) {
@@ -470,6 +499,8 @@ export class GrainAnalytics {
470
499
  }
471
500
  }
472
501
  startFlushTimer() {
502
+ if (typeof window === 'undefined')
503
+ return;
473
504
  if (this.flushTimer) {
474
505
  clearInterval(this.flushTimer);
475
506
  }
@@ -553,7 +584,12 @@ export class GrainAnalytics {
553
584
  */
554
585
  handleConsentGranted() {
555
586
  this.flushWaitingForConsentQueue();
556
- // Track consent granted event with mapping
587
+ // Initialize persistent ID now that consent is granted (if not already initialized)
588
+ if (!this.persistentAnonymousUserId) {
589
+ this.initializePersistentAnonymousUserId();
590
+ this.log('Initialized persistent ID after consent grant');
591
+ }
592
+ // Track consent granted event with mapping from ephemeral to persistent ID
557
593
  if (this.ephemeralSessionId) {
558
594
  this.trackSystemEvent('_grain_consent_granted', {
559
595
  previous_session_id: this.ephemeralSessionId,
@@ -723,19 +759,11 @@ export class GrainAnalytics {
723
759
  this.persistentAnonymousUserId = null;
724
760
  }
725
761
  else {
726
- // If clearing user ID, ensure we have a UUIDv4 identifier
762
+ // If clearing user ID, ensure we have an identifier
727
763
  if (!this.persistentAnonymousUserId) {
728
764
  this.persistentAnonymousUserId = this.generateAnonymousUserId();
729
- // Try to persist the new anonymous ID
730
- if (typeof window !== 'undefined') {
731
- try {
732
- const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
733
- localStorage.setItem(storageKey, this.persistentAnonymousUserId);
734
- }
735
- catch (error) {
736
- this.log('Failed to persist new anonymous user ID:', error);
737
- }
738
- }
765
+ // Try to persist the new anonymous ID (respects GDPR opt-in mode)
766
+ this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
739
767
  }
740
768
  }
741
769
  }
@@ -1316,6 +1344,8 @@ export class GrainAnalytics {
1316
1344
  * Start automatic configuration refresh timer
1317
1345
  */
1318
1346
  startConfigRefreshTimer() {
1347
+ if (typeof window === 'undefined')
1348
+ return;
1319
1349
  if (this.configRefreshTimer) {
1320
1350
  clearInterval(this.configRefreshTimer);
1321
1351
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grainql/analytics-web",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Lightweight TypeScript SDK for sending analytics events and managing remote configurations via Grain's REST API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -42,7 +42,10 @@
42
42
  "test:ci": "jest --ci --coverage",
43
43
  "prepublishOnly": "npm run clean && npm run build",
44
44
  "size": "npm run build && node scripts/bundle-analysis.js",
45
- "size:limit": "size-limit"
45
+ "size:limit": "size-limit",
46
+ "dev:mini-app": "cd mini-app && npm run dev",
47
+ "build:mini-app": "cd mini-app && npm run build",
48
+ "start:mini-app": "cd mini-app && npm run start"
46
49
  },
47
50
  "keywords": [
48
51
  "analytics",