mixpanel-browser 2.77.0 → 2.79.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 (68) hide show
  1. package/.claude/settings.local.json +6 -9
  2. package/.eslintrc.json +12 -0
  3. package/.github/workflows/openfeature-provider-tests.yml +31 -0
  4. package/CHANGELOG.md +11 -0
  5. package/build.sh +2 -2
  6. package/dist/async-modules/{mixpanel-recorder-wIWnMDLA.min.js → mixpanel-recorder-D5HJyV2E.min.js} +2 -2
  7. package/dist/async-modules/mixpanel-recorder-D5HJyV2E.min.js.map +1 -0
  8. package/dist/async-modules/{mixpanel-recorder-DLKbUIEE.js → mixpanel-recorder-P6SEnnPV.js} +57 -33
  9. package/dist/async-modules/mixpanel-targeting-1L9FyetZ.min.js +2 -0
  10. package/dist/async-modules/mixpanel-targeting-1L9FyetZ.min.js.map +1 -0
  11. package/dist/async-modules/{mixpanel-targeting-CmVvUyFM.js → mixpanel-targeting-BBMVbgJF.js} +24 -13
  12. package/dist/mixpanel-core.cjs.d.ts +46 -1
  13. package/dist/mixpanel-core.cjs.js +671 -272
  14. package/dist/mixpanel-recorder.js +57 -33
  15. package/dist/mixpanel-recorder.min.js +1 -1
  16. package/dist/mixpanel-recorder.min.js.map +1 -1
  17. package/dist/mixpanel-targeting.js +24 -13
  18. package/dist/mixpanel-targeting.min.js +1 -1
  19. package/dist/mixpanel-targeting.min.js.map +1 -1
  20. package/dist/mixpanel-with-async-modules.cjs.d.ts +46 -1
  21. package/dist/mixpanel-with-async-modules.cjs.js +673 -274
  22. package/dist/mixpanel-with-async-recorder.cjs.d.ts +46 -1
  23. package/dist/mixpanel-with-async-recorder.cjs.js +673 -274
  24. package/dist/mixpanel-with-recorder.d.ts +46 -1
  25. package/dist/mixpanel-with-recorder.js +596 -197
  26. package/dist/mixpanel-with-recorder.min.d.ts +46 -1
  27. package/dist/mixpanel-with-recorder.min.js +1 -1
  28. package/dist/mixpanel.amd.d.ts +46 -1
  29. package/dist/mixpanel.amd.js +596 -197
  30. package/dist/mixpanel.cjs.d.ts +46 -1
  31. package/dist/mixpanel.cjs.js +596 -197
  32. package/dist/mixpanel.globals.js +673 -274
  33. package/dist/mixpanel.min.js +200 -189
  34. package/dist/mixpanel.module.d.ts +46 -1
  35. package/dist/mixpanel.module.js +596 -197
  36. package/dist/mixpanel.umd.d.ts +46 -1
  37. package/dist/mixpanel.umd.js +596 -197
  38. package/package.json +1 -1
  39. package/packages/openfeature-web-provider/README.md +357 -0
  40. package/packages/openfeature-web-provider/package-lock.json +1636 -0
  41. package/packages/openfeature-web-provider/package.json +51 -0
  42. package/packages/openfeature-web-provider/rollup.config.browser.mjs +26 -0
  43. package/packages/openfeature-web-provider/src/MixpanelProvider.ts +302 -0
  44. package/packages/openfeature-web-provider/src/index.ts +1 -0
  45. package/packages/openfeature-web-provider/src/types.ts +72 -0
  46. package/packages/openfeature-web-provider/test/MixpanelProvider.spec.ts +484 -0
  47. package/packages/openfeature-web-provider/tsconfig.json +15 -0
  48. package/src/autocapture/index.js +7 -2
  49. package/src/config.js +1 -1
  50. package/src/flags/CLAUDE.md +24 -0
  51. package/src/flags/flags-persistence.js +176 -0
  52. package/src/flags/index.js +278 -98
  53. package/src/index.d.ts +46 -1
  54. package/src/mixpanel-core.js +27 -8
  55. package/src/recorder/idb-config.js +16 -0
  56. package/src/recorder/recording-registry.js +7 -2
  57. package/src/recorder/session-recording.js +9 -4
  58. package/src/recorder-manager.js +7 -2
  59. package/src/request-queue.js +1 -2
  60. package/src/shared-lock.js +2 -3
  61. package/src/storage/indexed-db.js +16 -15
  62. package/src/storage/local-storage.js +5 -3
  63. package/src/utils.js +25 -12
  64. package/testServer.js +2 -0
  65. package/tsconfig.base.json +9 -0
  66. package/dist/async-modules/mixpanel-recorder-wIWnMDLA.min.js.map +0 -1
  67. package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js +0 -2
  68. package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js.map +0 -1
@@ -0,0 +1,484 @@
1
+ import chai, { expect } from 'chai';
2
+ import sinon from 'sinon';
3
+ import sinonChai from 'sinon-chai';
4
+ import { MixpanelProvider } from '../src/MixpanelProvider';
5
+ import { FlagsManager } from '../src/types';
6
+ import { ErrorCode } from '@openfeature/web-sdk';
7
+
8
+ chai.use(sinonChai);
9
+
10
+ describe('MixpanelProvider', () => {
11
+ let mockFlagsManager: FlagsManager;
12
+ let mockFlags: Map<string, any>;
13
+ let mockLogger: any;
14
+ let provider: MixpanelProvider;
15
+
16
+ beforeEach(() => {
17
+ mockFlags = new Map();
18
+ mockFlagsManager = {
19
+ are_flags_ready: sinon.stub().returns(true),
20
+ load_flags: sinon.stub().resolves(),
21
+ get_variant: sinon.stub().resolves(),
22
+ get_variant_sync: sinon.stub().callsFake((key: string, fallback: any) => {
23
+ return mockFlags.get(key) || fallback;
24
+ }),
25
+ get_variant_value: sinon.stub().resolves(),
26
+ get_variant_value_sync: sinon.stub(),
27
+ is_enabled: sinon.stub().resolves(),
28
+ is_enabled_sync: sinon.stub(),
29
+ update_context: sinon.stub().resolves(),
30
+ when_ready: sinon.stub().resolves(),
31
+ };
32
+ mockLogger = {
33
+ debug: sinon.stub(),
34
+ info: sinon.stub(),
35
+ warn: sinon.stub(),
36
+ error: sinon.stub(),
37
+ };
38
+ provider = new MixpanelProvider(mockFlagsManager);
39
+ });
40
+
41
+ afterEach(() => {
42
+ sinon.restore();
43
+ });
44
+
45
+ describe('metadata', () => {
46
+ it('should have correct provider name', () => {
47
+ expect(provider.metadata.name).to.equal('mixpanel-provider');
48
+ });
49
+
50
+ it('should run on client', () => {
51
+ expect(provider.runsOn).to.equal('client');
52
+ });
53
+ });
54
+
55
+ describe('initialize', () => {
56
+ it('should wait for when_ready to resolve', async () => {
57
+ let readyResolved = false;
58
+ (mockFlagsManager.when_ready as sinon.SinonStub).returns(
59
+ new Promise<void>((resolve) => {
60
+ setTimeout(() => {
61
+ readyResolved = true;
62
+ resolve();
63
+ }, 10);
64
+ })
65
+ );
66
+
67
+ expect(readyResolved).to.be.false;
68
+ await provider.initialize();
69
+ expect(readyResolved).to.be.true;
70
+ });
71
+
72
+ it('should call update_context when context is provided', async () => {
73
+ const context = { userId: 'test-user', email: 'test@example.com' };
74
+
75
+ await provider.initialize(context);
76
+
77
+ expect(mockFlagsManager.update_context).to.have.been.calledOnce;
78
+ expect(mockFlagsManager.update_context).to.have.been.calledWith(context, { replace: true });
79
+ });
80
+
81
+ it('should not call update_context when context is empty', async () => {
82
+ await provider.initialize({});
83
+
84
+ expect(mockFlagsManager.update_context).not.to.have.been.called;
85
+ });
86
+
87
+ it('should not call update_context when context is undefined', async () => {
88
+ await provider.initialize();
89
+
90
+ expect(mockFlagsManager.update_context).not.to.have.been.called;
91
+ });
92
+
93
+ it('should call when_ready during initialize', async () => {
94
+ await provider.initialize();
95
+
96
+ expect(mockFlagsManager.when_ready).to.have.been.calledOnce;
97
+ });
98
+ });
99
+
100
+ describe('onContextChange', () => {
101
+ it('should call update_context with new context and replace: true', async () => {
102
+ const oldContext = { userId: 'old-user' };
103
+ const newContext = { userId: 'new-user', plan: 'premium' };
104
+
105
+ await provider.onContextChange(oldContext, newContext);
106
+
107
+ expect(mockFlagsManager.update_context).to.have.been.calledOnce;
108
+ expect(mockFlagsManager.update_context).to.have.been.calledWith(newContext, { replace: true });
109
+ });
110
+
111
+ it('should handle empty new context', async () => {
112
+ await provider.onContextChange({ userId: 'old' }, {});
113
+
114
+ expect(mockFlagsManager.update_context).to.have.been.calledWith({}, { replace: true });
115
+ });
116
+ });
117
+
118
+ describe('onClose', () => {
119
+ it('should resolve without error', async () => {
120
+ await provider.onClose();
121
+ });
122
+
123
+ it('should return a promise', () => {
124
+ const result = provider.onClose();
125
+
126
+ expect(result).to.be.instanceOf(Promise);
127
+ });
128
+ });
129
+
130
+ describe('resolveBooleanEvaluation', () => {
131
+ it('should return correct value when flag exists with boolean value', () => {
132
+ mockFlags.set('feature-enabled', { key: 'enabled', value: true });
133
+
134
+ const result = provider.resolveBooleanEvaluation('feature-enabled', false, {}, mockLogger);
135
+
136
+ expect(result.value).to.equal(true);
137
+ expect(result.variant).to.equal('enabled');
138
+ expect(result.reason).to.equal('TARGETING_MATCH');
139
+ expect(result.errorCode).to.be.undefined;
140
+ });
141
+
142
+ it('should return false boolean value correctly', () => {
143
+ mockFlags.set('feature-disabled', { key: 'disabled', value: false });
144
+
145
+ const result = provider.resolveBooleanEvaluation('feature-disabled', true, {}, mockLogger);
146
+
147
+ expect(result.value).to.equal(false);
148
+ expect(result.variant).to.equal('disabled');
149
+ });
150
+
151
+ it('should return TYPE_MISMATCH error when value is not boolean (string)', () => {
152
+ mockFlags.set('string-flag', { key: 'variant-a', value: 'not-a-boolean' });
153
+
154
+ const result = provider.resolveBooleanEvaluation('string-flag', false, {}, mockLogger);
155
+
156
+ expect(result.value).to.equal(false);
157
+ expect(result.errorCode).to.equal(ErrorCode.TYPE_MISMATCH);
158
+ expect(result.errorMessage).to.include('not a boolean');
159
+ expect(result.reason).to.equal('ERROR');
160
+ });
161
+
162
+ it('should return TYPE_MISMATCH error when value is not boolean (number)', () => {
163
+ mockFlags.set('number-flag', { key: 'variant-a', value: 42 });
164
+
165
+ const result = provider.resolveBooleanEvaluation('number-flag', true, {}, mockLogger);
166
+
167
+ expect(result.value).to.equal(true);
168
+ expect(result.errorCode).to.equal(ErrorCode.TYPE_MISMATCH);
169
+ });
170
+
171
+ it('should return TYPE_MISMATCH error when value is not boolean (object)', () => {
172
+ mockFlags.set('object-flag', { key: 'variant-a', value: { some: 'object' } });
173
+
174
+ const result = provider.resolveBooleanEvaluation('object-flag', false, {}, mockLogger);
175
+
176
+ expect(result.value).to.equal(false);
177
+ expect(result.errorCode).to.equal(ErrorCode.TYPE_MISMATCH);
178
+ });
179
+ });
180
+
181
+ describe('resolveStringEvaluation', () => {
182
+ it('should return correct value when flag exists with string value', () => {
183
+ mockFlags.set('theme-flag', { key: 'dark', value: 'dark-mode' });
184
+
185
+ const result = provider.resolveStringEvaluation('theme-flag', 'light-mode', {}, mockLogger);
186
+
187
+ expect(result.value).to.equal('dark-mode');
188
+ expect(result.variant).to.equal('dark');
189
+ expect(result.reason).to.equal('TARGETING_MATCH');
190
+ expect(result.errorCode).to.be.undefined;
191
+ });
192
+
193
+ it('should return empty string value correctly', () => {
194
+ mockFlags.set('empty-string-flag', { key: 'empty', value: '' });
195
+
196
+ const result = provider.resolveStringEvaluation('empty-string-flag', 'default', {}, mockLogger);
197
+
198
+ expect(result.value).to.equal('');
199
+ expect(result.variant).to.equal('empty');
200
+ });
201
+
202
+ it('should return TYPE_MISMATCH error when value is not string (boolean)', () => {
203
+ mockFlags.set('bool-flag', { key: 'variant-a', value: true });
204
+
205
+ const result = provider.resolveStringEvaluation('bool-flag', 'default', {}, mockLogger);
206
+
207
+ expect(result.value).to.equal('default');
208
+ expect(result.errorCode).to.equal(ErrorCode.TYPE_MISMATCH);
209
+ expect(result.errorMessage).to.include('not a string');
210
+ expect(result.reason).to.equal('ERROR');
211
+ });
212
+
213
+ it('should return TYPE_MISMATCH error when value is not string (number)', () => {
214
+ mockFlags.set('number-flag', { key: 'variant-a', value: 123 });
215
+
216
+ const result = provider.resolveStringEvaluation('number-flag', 'default', {}, mockLogger);
217
+
218
+ expect(result.value).to.equal('default');
219
+ expect(result.errorCode).to.equal(ErrorCode.TYPE_MISMATCH);
220
+ });
221
+
222
+ it('should return TYPE_MISMATCH error when value is not string (object)', () => {
223
+ mockFlags.set('object-flag', { key: 'variant-a', value: { key: 'value' } });
224
+
225
+ const result = provider.resolveStringEvaluation('object-flag', 'default', {}, mockLogger);
226
+
227
+ expect(result.value).to.equal('default');
228
+ expect(result.errorCode).to.equal(ErrorCode.TYPE_MISMATCH);
229
+ });
230
+ });
231
+
232
+ describe('resolveNumberEvaluation', () => {
233
+ it('should return correct value when flag exists with number value', () => {
234
+ mockFlags.set('percentage-flag', { key: 'variant-50', value: 50 });
235
+
236
+ const result = provider.resolveNumberEvaluation('percentage-flag', 0, {}, mockLogger);
237
+
238
+ expect(result.value).to.equal(50);
239
+ expect(result.variant).to.equal('variant-50');
240
+ expect(result.reason).to.equal('TARGETING_MATCH');
241
+ expect(result.errorCode).to.be.undefined;
242
+ });
243
+
244
+ it('should return zero value correctly', () => {
245
+ mockFlags.set('zero-flag', { key: 'zero', value: 0 });
246
+
247
+ const result = provider.resolveNumberEvaluation('zero-flag', 100, {}, mockLogger);
248
+
249
+ expect(result.value).to.equal(0);
250
+ expect(result.variant).to.equal('zero');
251
+ });
252
+
253
+ it('should return negative number correctly', () => {
254
+ mockFlags.set('negative-flag', { key: 'negative', value: -42 });
255
+
256
+ const result = provider.resolveNumberEvaluation('negative-flag', 0, {}, mockLogger);
257
+
258
+ expect(result.value).to.equal(-42);
259
+ });
260
+
261
+ it('should return float value correctly', () => {
262
+ mockFlags.set('float-flag', { key: 'float', value: 3.14159 });
263
+
264
+ const result = provider.resolveNumberEvaluation('float-flag', 0, {}, mockLogger);
265
+
266
+ expect(result.value).to.equal(3.14159);
267
+ });
268
+
269
+ it('should return TYPE_MISMATCH error when value is not number (string)', () => {
270
+ mockFlags.set('string-flag', { key: 'variant-a', value: '42' });
271
+
272
+ const result = provider.resolveNumberEvaluation('string-flag', 0, {}, mockLogger);
273
+
274
+ expect(result.value).to.equal(0);
275
+ expect(result.errorCode).to.equal(ErrorCode.TYPE_MISMATCH);
276
+ expect(result.errorMessage).to.include('not a number');
277
+ expect(result.reason).to.equal('ERROR');
278
+ });
279
+
280
+ it('should return TYPE_MISMATCH error when value is not number (boolean)', () => {
281
+ mockFlags.set('bool-flag', { key: 'variant-a', value: true });
282
+
283
+ const result = provider.resolveNumberEvaluation('bool-flag', 0, {}, mockLogger);
284
+
285
+ expect(result.value).to.equal(0);
286
+ expect(result.errorCode).to.equal(ErrorCode.TYPE_MISMATCH);
287
+ });
288
+
289
+ it('should return TYPE_MISMATCH error when value is not number (object)', () => {
290
+ mockFlags.set('object-flag', { key: 'variant-a', value: { num: 42 } });
291
+
292
+ const result = provider.resolveNumberEvaluation('object-flag', 0, {}, mockLogger);
293
+
294
+ expect(result.value).to.equal(0);
295
+ expect(result.errorCode).to.equal(ErrorCode.TYPE_MISMATCH);
296
+ });
297
+ });
298
+
299
+ describe('resolveObjectEvaluation', () => {
300
+ it('should return correct value when flag exists with object value', () => {
301
+ const objectValue = { feature: 'enabled', level: 2, options: ['a', 'b'] };
302
+ mockFlags.set('config-flag', { key: 'variant-full', value: objectValue });
303
+
304
+ const result = provider.resolveObjectEvaluation('config-flag', {}, {}, mockLogger);
305
+
306
+ expect(result.value).to.deep.equal(objectValue);
307
+ expect(result.variant).to.equal('variant-full');
308
+ expect(result.reason).to.equal('TARGETING_MATCH');
309
+ expect(result.errorCode).to.be.undefined;
310
+ });
311
+
312
+ it('should return empty object value correctly', () => {
313
+ mockFlags.set('empty-object-flag', { key: 'empty', value: {} });
314
+
315
+ const result = provider.resolveObjectEvaluation('empty-object-flag', { default: true }, {}, mockLogger);
316
+
317
+ expect(result.value).to.deep.equal({});
318
+ expect(result.variant).to.equal('empty');
319
+ });
320
+
321
+ it('should return array value correctly (object accepts any type)', () => {
322
+ const arrayValue = [1, 2, 3, 'four'];
323
+ mockFlags.set('array-flag', { key: 'array-variant', value: arrayValue });
324
+
325
+ const result = provider.resolveObjectEvaluation('array-flag', [], {}, mockLogger);
326
+
327
+ expect(result.value).to.deep.equal(arrayValue);
328
+ expect(result.variant).to.equal('array-variant');
329
+ expect(result.errorCode).to.be.undefined;
330
+ });
331
+
332
+ it('should return nested object value correctly', () => {
333
+ const nestedValue = { level1: { level2: { level3: 'deep' } } };
334
+ mockFlags.set('nested-flag', { key: 'nested', value: nestedValue });
335
+
336
+ const result = provider.resolveObjectEvaluation('nested-flag', {}, {}, mockLogger);
337
+
338
+ expect(result.value).to.deep.equal(nestedValue);
339
+ });
340
+
341
+ it('should return string value via object evaluation (object accepts any type)', () => {
342
+ mockFlags.set('string-flag', { key: 'variant-a', value: 'not-an-object' });
343
+
344
+ const result = provider.resolveObjectEvaluation('string-flag', {}, {}, mockLogger);
345
+
346
+ expect(result.value).to.equal('not-an-object');
347
+ expect(result.variant).to.equal('variant-a');
348
+ expect(result.errorCode).to.be.undefined;
349
+ });
350
+
351
+ it('should return number value via object evaluation (object accepts any type)', () => {
352
+ mockFlags.set('number-flag', { key: 'variant-a', value: 42 });
353
+
354
+ const result = provider.resolveObjectEvaluation('number-flag', {}, {}, mockLogger);
355
+
356
+ expect(result.value).to.equal(42);
357
+ expect(result.errorCode).to.be.undefined;
358
+ });
359
+
360
+ it('should return boolean value via object evaluation (object accepts any type)', () => {
361
+ mockFlags.set('bool-flag', { key: 'variant-a', value: true });
362
+
363
+ const result = provider.resolveObjectEvaluation('bool-flag', {}, {}, mockLogger);
364
+
365
+ expect(result.value).to.equal(true);
366
+ expect(result.errorCode).to.be.undefined;
367
+ });
368
+
369
+ it('should return null value via object evaluation (object accepts any type)', () => {
370
+ mockFlags.set('null-flag', { key: 'variant-a', value: null });
371
+
372
+ const result = provider.resolveObjectEvaluation('null-flag', { default: true }, {}, mockLogger);
373
+
374
+ expect(result.value).to.be.null;
375
+ expect(result.variant).to.equal('variant-a');
376
+ expect(result.errorCode).to.be.undefined;
377
+ });
378
+ });
379
+
380
+ // FLAG_NOT_FOUND and PROVIDER_NOT_READY share the same code path (resolveFlag)
381
+ // across all resolve methods — test them once parameterized rather than 4x each.
382
+ describe('shared error behavior', () => {
383
+ const resolveMethodCases = [
384
+ { method: 'resolveBooleanEvaluation' as const, defaultValue: false },
385
+ { method: 'resolveStringEvaluation' as const, defaultValue: 'fallback' },
386
+ { method: 'resolveNumberEvaluation' as const, defaultValue: 99 },
387
+ { method: 'resolveObjectEvaluation' as const, defaultValue: { fallback: true } },
388
+ ];
389
+
390
+ resolveMethodCases.forEach(({ method, defaultValue }) => {
391
+ it(`${method} should return FLAG_NOT_FOUND when flag does not exist`, () => {
392
+ const result = (provider as any)[method]('non-existent-flag', defaultValue, {}, mockLogger);
393
+
394
+ expect(result.value).to.deep.equal(defaultValue);
395
+ expect(result.errorCode).to.equal(ErrorCode.FLAG_NOT_FOUND);
396
+ expect(result.errorMessage).to.include('not found');
397
+ expect(result.reason).to.equal('DEFAULT');
398
+ });
399
+ });
400
+
401
+ resolveMethodCases.forEach(({ method, defaultValue }) => {
402
+ it(`${method} should return PROVIDER_NOT_READY when flags not loaded`, () => {
403
+ (mockFlagsManager.are_flags_ready as sinon.SinonStub).returns(false);
404
+
405
+ const result = (provider as any)[method]('any-flag', defaultValue, {}, mockLogger);
406
+
407
+ expect(result.value).to.deep.equal(defaultValue);
408
+ expect(result.errorCode).to.equal(ErrorCode.PROVIDER_NOT_READY);
409
+ expect(result.errorMessage).to.include('not been loaded');
410
+ expect(result.reason).to.equal('ERROR');
411
+ });
412
+ });
413
+ });
414
+
415
+ describe('edge cases', () => {
416
+ it('should handle flag with experiment metadata', () => {
417
+ mockFlags.set('experiment-flag', {
418
+ key: 'treatment',
419
+ value: true,
420
+ experiment_id: 'exp-123',
421
+ is_experiment_active: true,
422
+ is_qa_tester: false,
423
+ });
424
+
425
+ const result = provider.resolveBooleanEvaluation('experiment-flag', false, {}, mockLogger);
426
+
427
+ expect(result.value).to.equal(true);
428
+ expect(result.variant).to.equal('treatment');
429
+ });
430
+
431
+ it('should handle special characters in flag key', () => {
432
+ mockFlags.set('flag-with-special_chars.and/slashes', { key: 'variant', value: 'special' });
433
+
434
+ const result = provider.resolveStringEvaluation(
435
+ 'flag-with-special_chars.and/slashes',
436
+ 'default',
437
+ {},
438
+ mockLogger
439
+ );
440
+
441
+ expect(result.value).to.equal('special');
442
+ });
443
+
444
+ // NaN passes typeof === 'number', documenting this intentional passthrough
445
+ it('should handle NaN as valid number (typeof NaN === number)', () => {
446
+ mockFlags.set('nan-flag', { key: 'nan', value: NaN });
447
+
448
+ const result = provider.resolveNumberEvaluation('nan-flag', 0, {}, mockLogger);
449
+
450
+ expect(result.value).to.be.NaN;
451
+ expect(result.errorCode).to.be.undefined;
452
+ });
453
+
454
+ it('should handle Infinity as valid number', () => {
455
+ mockFlags.set('infinity-flag', { key: 'infinity', value: Infinity });
456
+
457
+ const result = provider.resolveNumberEvaluation('infinity-flag', 0, {}, mockLogger);
458
+
459
+ expect(result.value).to.equal(Infinity);
460
+ });
461
+
462
+ it('should handle evaluation context passed to methods', () => {
463
+ mockFlags.set('context-flag', { key: 'variant', value: true });
464
+
465
+ const context = { userId: 'user-123', plan: 'premium' };
466
+ const result = provider.resolveBooleanEvaluation('context-flag', false, context, mockLogger);
467
+
468
+ expect(result.value).to.equal(true);
469
+ });
470
+
471
+ it('should create new provider instances independently', () => {
472
+ const provider1 = new MixpanelProvider(mockFlagsManager);
473
+ const provider2 = new MixpanelProvider(mockFlagsManager);
474
+
475
+ mockFlags.set('test-flag', { key: 'v', value: 'test' });
476
+
477
+ provider1.resolveStringEvaluation('test-flag', '', {}, mockLogger);
478
+ expect(mockFlagsManager.get_variant_sync).to.have.been.calledOnce;
479
+
480
+ provider2.resolveStringEvaluation('test-flag', '', {}, mockLogger);
481
+ expect(mockFlagsManager.get_variant_sync).to.have.been.calledTwice;
482
+ });
483
+ });
484
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "target": "ES2018",
5
+ "module": "commonjs",
6
+ "lib": ["ES2018", "DOM"],
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src"
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist", "test"]
15
+ }
@@ -364,14 +364,15 @@ Autocapture.prototype.initInputTracking = function() {
364
364
  Autocapture.prototype.initPageviewTracking = function() {
365
365
  window.removeEventListener(EV_MP_LOCATION_CHANGE, this.listenerLocationchange);
366
366
 
367
- if (!this.pageviewTrackingConfig()) {
367
+ if (!this.pageviewTrackingConfig() && !this.mp.get_config('record_heatmap_data')) {
368
368
  return;
369
369
  }
370
370
  logger.log('Initializing pageview tracking');
371
371
 
372
372
  var previousTrackedUrl = '';
373
373
  var tracked = false;
374
- if (!this.currentUrlBlocked()) {
374
+ // Track initial pageview if pageview tracking enabled OR heatmap recording is active
375
+ if ((this.pageviewTrackingConfig() || this.mp.is_recording_heatmap_data()) && !this.currentUrlBlocked()) {
375
376
  tracked = this.mp.track_pageview(DEFAULT_PROPS);
376
377
  }
377
378
  if (tracked) {
@@ -387,6 +388,10 @@ Autocapture.prototype.initPageviewTracking = function() {
387
388
  var shouldTrack = false;
388
389
  var didPathChange = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
389
390
  var trackPageviewOption = this.pageviewTrackingConfig();
391
+ if (!trackPageviewOption && this.mp.is_recording_heatmap_data()) {
392
+ trackPageviewOption = PAGEVIEW_OPTION_FULL_URL;
393
+ }
394
+
390
395
  if (trackPageviewOption === PAGEVIEW_OPTION_FULL_URL) {
391
396
  shouldTrack = currentUrl !== previousTrackedUrl;
392
397
  } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING) {
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.77.0'
3
+ LIB_VERSION: '2.79.0'
4
4
  };
5
5
 
6
6
  // Window global names for async modules
@@ -0,0 +1,24 @@
1
+ # Flags Module
2
+
3
+ ## Testing
4
+ - Test runner is **mocha** (not jest): `BABEL_ENV=test npx mocha --require babel-core/register tests/unit/flags.js`
5
+ - Unit tests live in `tests/unit/flags.js`
6
+
7
+ ## Code style
8
+ - ES5 prototypal classes — no arrow functions, no ES6 classes
9
+ - Use `.bind(this)` for `this` context in promise `.then()` / `.catch()` callbacks
10
+ - Flat promise chains preferred over nested `.then()` inside `.then()`
11
+
12
+ ## Public API pattern
13
+ - Snake-case aliases are registered at the bottom of `index.js` (e.g., `prototype['load_flags'] = prototype.loadFlags`)
14
+ - New public methods need both the camelCase implementation and a snake_case alias
15
+
16
+ ## Error handling convention
17
+ - `fetchFlags()` always rejects on error (single `.catch` that logs and re-throws)
18
+ - Fire-and-forget callers (`init`, `updateContext`, `mixpanel-core.js` identify call) swallow errors at the call site with `.catch(function() {})`
19
+ - User-facing methods like `loadFlags` propagate rejections so the caller can handle them
20
+
21
+ ## Key files
22
+ - `src/flags/index.js` — `FeatureFlagManager` class (fetch, load, variants, first-time events)
23
+ - `src/mixpanel-core.js` — calls `fetchFlags()` on distinct_id change (~line 1764)
24
+ - `tests/unit/flags.js` — all flag unit tests