mixpanel-browser 2.78.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 (66) hide show
  1. package/.claude/settings.local.json +6 -11
  2. package/.eslintrc.json +12 -0
  3. package/.github/workflows/openfeature-provider-tests.yml +31 -0
  4. package/CHANGELOG.md +8 -1
  5. package/build.sh +2 -2
  6. package/dist/async-modules/{mixpanel-recorder-BjSlYaNJ.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-zMBXIyeG.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-UHf4eBfC.js → mixpanel-targeting-BBMVbgJF.js} +24 -13
  12. package/dist/mixpanel-core.cjs.d.ts +45 -1
  13. package/dist/mixpanel-core.cjs.js +565 -197
  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 +45 -1
  21. package/dist/mixpanel-with-async-modules.cjs.js +567 -199
  22. package/dist/mixpanel-with-async-recorder.cjs.d.ts +45 -1
  23. package/dist/mixpanel-with-async-recorder.cjs.js +567 -199
  24. package/dist/mixpanel-with-recorder.d.ts +45 -1
  25. package/dist/mixpanel-with-recorder.js +490 -122
  26. package/dist/mixpanel-with-recorder.min.d.ts +45 -1
  27. package/dist/mixpanel-with-recorder.min.js +1 -1
  28. package/dist/mixpanel.amd.d.ts +45 -1
  29. package/dist/mixpanel.amd.js +490 -122
  30. package/dist/mixpanel.cjs.d.ts +45 -1
  31. package/dist/mixpanel.cjs.js +490 -122
  32. package/dist/mixpanel.globals.js +567 -199
  33. package/dist/mixpanel.min.js +199 -189
  34. package/dist/mixpanel.module.d.ts +45 -1
  35. package/dist/mixpanel.module.js +490 -122
  36. package/dist/mixpanel.umd.d.ts +45 -1
  37. package/dist/mixpanel.umd.js +490 -122
  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/flags-persistence.js +176 -0
  51. package/src/flags/index.js +174 -23
  52. package/src/index.d.ts +45 -1
  53. package/src/mixpanel-core.js +24 -7
  54. package/src/recorder/idb-config.js +16 -0
  55. package/src/recorder/recording-registry.js +7 -2
  56. package/src/recorder/session-recording.js +9 -4
  57. package/src/recorder-manager.js +7 -2
  58. package/src/request-queue.js +1 -2
  59. package/src/shared-lock.js +2 -3
  60. package/src/storage/indexed-db.js +16 -15
  61. package/src/storage/local-storage.js +5 -3
  62. package/src/utils.js +25 -12
  63. package/tsconfig.base.json +9 -0
  64. package/dist/async-modules/mixpanel-recorder-BjSlYaNJ.min.js.map +0 -1
  65. package/dist/async-modules/mixpanel-targeting-BSHal4N9.min.js +0 -2
  66. package/dist/async-modules/mixpanel-targeting-BSHal4N9.min.js.map +0 -1
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@mixpanel/openfeature-web-provider",
3
+ "version": "0.1.0",
4
+ "description": "OpenFeature Web Provider for Mixpanel feature flags",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "build:browser": "npx rollup -c rollup.config.browser.mjs",
13
+ "build:all": "npm run build && npm run build:browser",
14
+ "test": "mocha --require ts-node/register test/**/*.spec.ts",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "peerDependencies": {
18
+ "@openfeature/web-sdk": "^1.0.0",
19
+ "mixpanel-browser": "^2.78.0"
20
+ },
21
+ "devDependencies": {
22
+ "@openfeature/web-sdk": "1.7.2",
23
+ "@types/chai": "4.3.20",
24
+ "@types/mocha": "10.0.10",
25
+ "@types/node": "20.19.33",
26
+ "@types/sinon": "17.0.4",
27
+ "@types/sinon-chai": "3.2.12",
28
+ "chai": "4.5.0",
29
+ "mixpanel-browser": "^2.78.0",
30
+ "mocha": "10.8.2",
31
+ "sinon": "17.0.1",
32
+ "sinon-chai": "3.7.0",
33
+ "ts-node": "10.9.2",
34
+ "typescript": "5.9.3"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/mixpanel/mixpanel-js.git",
39
+ "directory": "packages/openfeature-web-provider"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "license": "Apache-2.0",
45
+ "keywords": [
46
+ "openfeature",
47
+ "mixpanel",
48
+ "feature-flags",
49
+ "provider"
50
+ ]
51
+ }
@@ -0,0 +1,26 @@
1
+ import resolve from '@rollup/plugin-node-resolve';
2
+ import commonjs from '@rollup/plugin-commonjs';
3
+ import esbuild from 'rollup-plugin-esbuild';
4
+
5
+ export default {
6
+ input: 'src/index.ts',
7
+ output: {
8
+ file: 'dist/browser.js',
9
+ format: 'iife',
10
+ name: 'MixpanelOpenFeatureProvider',
11
+ globals: {
12
+ '@openfeature/web-sdk': 'OpenFeature'
13
+ },
14
+ sourcemap: true
15
+ },
16
+ external: ['@openfeature/web-sdk'],
17
+ plugins: [
18
+ resolve({
19
+ extensions: ['.ts', '.js']
20
+ }),
21
+ commonjs(),
22
+ esbuild({
23
+ target: 'es2018'
24
+ })
25
+ ]
26
+ };
@@ -0,0 +1,302 @@
1
+ import type {
2
+ EvaluationContext,
3
+ Provider,
4
+ ResolutionDetails,
5
+ Logger,
6
+ ProviderMetadata,
7
+ JsonValue,
8
+ } from '@openfeature/web-sdk';
9
+ import { ErrorCode } from '@openfeature/web-sdk';
10
+ import mixpanel from 'mixpanel-browser';
11
+ import type { Config, Mixpanel, FlagsVariant } from 'mixpanel-browser';
12
+ import {
13
+ FlagsManager,
14
+ isBoolean,
15
+ isString,
16
+ isNumber,
17
+ createResolutionDetails,
18
+ createErrorResolutionDetails,
19
+ } from './types';
20
+
21
+ let _instanceCount = 0;
22
+
23
+ /**
24
+ * OpenFeature Web Provider for Mixpanel feature flags.
25
+ *
26
+ * This provider wraps the Mixpanel SDK's feature flags, allowing users
27
+ * to use the standardized OpenFeature API with Mixpanel as the backend.
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * import mixpanel from 'mixpanel-browser';
32
+ * import { OpenFeature } from '@openfeature/web-sdk';
33
+ * import { MixpanelProvider } from '@mixpanel/openfeature-web-provider';
34
+ *
35
+ * // Initialize Mixpanel with flags and context
36
+ * mixpanel.init('TOKEN', {
37
+ * flags: {
38
+ * context: { plan: 'premium' }
39
+ * }
40
+ * });
41
+ *
42
+ * // Register provider with flags manager
43
+ * await OpenFeature.setProviderAndWait(new MixpanelProvider(mixpanel.flags));
44
+ *
45
+ * // Use flags
46
+ * const client = OpenFeature.getClient();
47
+ * const showNewUI = client.getBooleanValue('new-ui', false);
48
+ * ```
49
+ */
50
+ export class MixpanelProvider implements Provider {
51
+ readonly metadata: ProviderMetadata = {
52
+ name: 'mixpanel-provider',
53
+ };
54
+
55
+ readonly runsOn = 'client' as const;
56
+
57
+ private readonly flags: FlagsManager;
58
+
59
+ /**
60
+ * The underlying Mixpanel instance, set when using the static `create` method.
61
+ * Users need this to call `identify()` and `track()` on the underlying instance.
62
+ */
63
+ mixpanel?: Mixpanel;
64
+
65
+ /**
66
+ * Creates a MixpanelProvider by initializing a new Mixpanel instance internally.
67
+ *
68
+ * The created Mixpanel instance is accessible via the `mixpanel` property
69
+ * for calling `identify()`, `track()`, and other Mixpanel methods.
70
+ *
71
+ * @param token - Your Mixpanel project token
72
+ * @param config - Optional Mixpanel configuration options
73
+ * @returns A MixpanelProvider with an initialized Mixpanel instance
74
+ */
75
+ static create(token: string, config?: Partial<Config>): MixpanelProvider {
76
+ const instanceName = `openfeature_${_instanceCount++}`;
77
+ const instance = mixpanel.init(token, config || {}, instanceName);
78
+ const flagsManager = (instance as any).flags as FlagsManager;
79
+ const provider = new MixpanelProvider(flagsManager);
80
+ provider.mixpanel = instance;
81
+ return provider;
82
+ }
83
+
84
+ /**
85
+ * Creates a new MixpanelProvider instance.
86
+ *
87
+ * @param flagsManager - The Mixpanel FlagsManager instance
88
+ */
89
+ constructor(flagsManager: FlagsManager) {
90
+ if (!flagsManager) {
91
+ throw new Error('FlagsManager is required');
92
+ }
93
+ // Validate required methods
94
+ if (typeof flagsManager.are_flags_ready !== 'function' ||
95
+ typeof flagsManager.get_variant_sync !== 'function' ||
96
+ typeof flagsManager.update_context !== 'function' ||
97
+ typeof flagsManager.when_ready !== 'function') {
98
+ throw new Error('Invalid FlagsManager: missing required methods');
99
+ }
100
+ this.flags = flagsManager;
101
+ }
102
+
103
+ /**
104
+ * Initialize the provider by waiting for Mixpanel's flags to be fetched.
105
+ */
106
+ async initialize(context?: EvaluationContext): Promise<void> {
107
+ // If context is provided, update Mixpanel's flag context
108
+ if (context && Object.keys(context).length > 0) {
109
+ await this.flags.update_context(context, { replace: true });
110
+ }
111
+
112
+ // Wait for the initial fetch to complete
113
+ await this.flags.when_ready();
114
+ }
115
+
116
+ /**
117
+ * Handle context changes by updating Mixpanel's flag context.
118
+ */
119
+ async onContextChange(
120
+ oldContext: EvaluationContext,
121
+ newContext: EvaluationContext
122
+ ): Promise<void> {
123
+ // Pass the new context directly to Mixpanel (replace mode)
124
+ await this.flags.update_context(newContext, { replace: true });
125
+ }
126
+
127
+ /**
128
+ * Clean up when the provider is closed.
129
+ */
130
+ async onClose(): Promise<void> {
131
+ // No cleanup needed - Mixpanel SDK manages its own lifecycle
132
+ }
133
+
134
+ /**
135
+ * Resolve a boolean flag value.
136
+ */
137
+ resolveBooleanEvaluation(
138
+ flagKey: string,
139
+ defaultValue: boolean,
140
+ _context: EvaluationContext,
141
+ _logger: Logger
142
+ ): ResolutionDetails<boolean> {
143
+ try {
144
+ const result = this.resolveFlag(flagKey, defaultValue);
145
+ if (result.errorCode) {
146
+ return result as ResolutionDetails<boolean>;
147
+ }
148
+
149
+ const value = result.value;
150
+ if (!isBoolean(value)) {
151
+ return createErrorResolutionDetails(
152
+ defaultValue,
153
+ ErrorCode.TYPE_MISMATCH,
154
+ `Flag "${flagKey}" value is not a boolean: ${typeof value}`
155
+ );
156
+ }
157
+
158
+ return createResolutionDetails(value, result.variant);
159
+ } catch (e) {
160
+ return createErrorResolutionDetails(
161
+ defaultValue,
162
+ ErrorCode.GENERAL,
163
+ e instanceof Error ? e.message : String(e)
164
+ );
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Resolve a string flag value.
170
+ */
171
+ resolveStringEvaluation(
172
+ flagKey: string,
173
+ defaultValue: string,
174
+ _context: EvaluationContext,
175
+ _logger: Logger
176
+ ): ResolutionDetails<string> {
177
+ try {
178
+ const result = this.resolveFlag(flagKey, defaultValue);
179
+ if (result.errorCode) {
180
+ return result as ResolutionDetails<string>;
181
+ }
182
+
183
+ const value = result.value;
184
+ if (!isString(value)) {
185
+ return createErrorResolutionDetails(
186
+ defaultValue,
187
+ ErrorCode.TYPE_MISMATCH,
188
+ `Flag "${flagKey}" value is not a string: ${typeof value}`
189
+ );
190
+ }
191
+
192
+ return createResolutionDetails(value, result.variant);
193
+ } catch (e) {
194
+ return createErrorResolutionDetails(
195
+ defaultValue,
196
+ ErrorCode.GENERAL,
197
+ e instanceof Error ? e.message : String(e)
198
+ );
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Resolve a number flag value.
204
+ */
205
+ resolveNumberEvaluation(
206
+ flagKey: string,
207
+ defaultValue: number,
208
+ _context: EvaluationContext,
209
+ _logger: Logger
210
+ ): ResolutionDetails<number> {
211
+ try {
212
+ const result = this.resolveFlag(flagKey, defaultValue);
213
+ if (result.errorCode) {
214
+ return result as ResolutionDetails<number>;
215
+ }
216
+
217
+ const value = result.value;
218
+ if (!isNumber(value)) {
219
+ return createErrorResolutionDetails(
220
+ defaultValue,
221
+ ErrorCode.TYPE_MISMATCH,
222
+ `Flag "${flagKey}" value is not a number: ${typeof value}`
223
+ );
224
+ }
225
+
226
+ return createResolutionDetails(value, result.variant);
227
+ } catch (e) {
228
+ return createErrorResolutionDetails(
229
+ defaultValue,
230
+ ErrorCode.GENERAL,
231
+ e instanceof Error ? e.message : String(e)
232
+ );
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Resolve an object flag value.
238
+ */
239
+ resolveObjectEvaluation<T extends JsonValue>(
240
+ flagKey: string,
241
+ defaultValue: T,
242
+ _context: EvaluationContext,
243
+ _logger: Logger
244
+ ): ResolutionDetails<T> {
245
+ try {
246
+ const result = this.resolveFlag(flagKey, defaultValue);
247
+ if (result.errorCode) {
248
+ return result as ResolutionDetails<T>;
249
+ }
250
+
251
+ return createResolutionDetails(result.value as T, result.variant);
252
+ } catch (e) {
253
+ return createErrorResolutionDetails(
254
+ defaultValue,
255
+ ErrorCode.GENERAL,
256
+ e instanceof Error ? e.message : String(e)
257
+ );
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Internal method to resolve a flag value from Mixpanel.
263
+ */
264
+ private resolveFlag<T>(
265
+ flagKey: string,
266
+ defaultValue: T
267
+ ): ResolutionDetails<any> {
268
+ // Check if flags are ready
269
+ if (!this.flags.are_flags_ready()) {
270
+ return createErrorResolutionDetails(
271
+ defaultValue,
272
+ ErrorCode.PROVIDER_NOT_READY,
273
+ 'Mixpanel flags have not been loaded yet'
274
+ );
275
+ }
276
+
277
+ // Create a fallback variant to detect if flag wasn't found
278
+ const fallbackVariant: FlagsVariant = {
279
+ key: flagKey,
280
+ value: defaultValue,
281
+ };
282
+
283
+ // Use get_variant_sync which triggers exposure tracking
284
+ const variant = this.flags.get_variant_sync(flagKey, fallbackVariant);
285
+
286
+ // Check if we got our fallback back (flag not found)
287
+ if (variant === fallbackVariant) {
288
+ return {
289
+ value: defaultValue,
290
+ errorCode: ErrorCode.FLAG_NOT_FOUND,
291
+ errorMessage: `Flag "${flagKey}" not found`,
292
+ reason: 'DEFAULT',
293
+ };
294
+ }
295
+
296
+ return {
297
+ value: variant.value,
298
+ variant: variant.key,
299
+ reason: 'TARGETING_MATCH',
300
+ };
301
+ }
302
+ }
@@ -0,0 +1 @@
1
+ export { MixpanelProvider } from './MixpanelProvider';
@@ -0,0 +1,72 @@
1
+ import type {
2
+ ResolutionDetails,
3
+ ErrorCode,
4
+ } from '@openfeature/web-sdk';
5
+ import type { FlagsManager as MixpanelFlagsManager } from 'mixpanel-browser';
6
+
7
+ /**
8
+ * Extended FlagsManager interface that includes when_ready().
9
+ * TODO: Remove this once mixpanel-browser exports when_ready() in its type definitions,
10
+ * and re-export FlagsManager directly from mixpanel-browser.
11
+ */
12
+ export interface FlagsManager extends MixpanelFlagsManager {
13
+ when_ready(): Promise<void>;
14
+ }
15
+
16
+ /**
17
+ * Type guard to check if a value is a boolean
18
+ */
19
+ export function isBoolean(value: unknown): value is boolean {
20
+ return typeof value === 'boolean';
21
+ }
22
+
23
+ /**
24
+ * Type guard to check if a value is a string
25
+ */
26
+ export function isString(value: unknown): value is string {
27
+ return typeof value === 'string';
28
+ }
29
+
30
+ /**
31
+ * Type guard to check if a value is a number
32
+ */
33
+ export function isNumber(value: unknown): value is number {
34
+ return typeof value === 'number';
35
+ }
36
+
37
+ /**
38
+ * Type guard to check if a value is an object (non-null)
39
+ */
40
+ export function isObject(value: unknown): value is Record<string, unknown> {
41
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
42
+ }
43
+
44
+ /**
45
+ * Helper to create a successful ResolutionDetails
46
+ */
47
+ export function createResolutionDetails<T>(
48
+ value: T,
49
+ variant?: string
50
+ ): ResolutionDetails<T> {
51
+ return {
52
+ value,
53
+ variant,
54
+ reason: 'TARGETING_MATCH',
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Helper to create an error ResolutionDetails
60
+ */
61
+ export function createErrorResolutionDetails<T>(
62
+ defaultValue: T,
63
+ errorCode: ErrorCode,
64
+ errorMessage?: string
65
+ ): ResolutionDetails<T> {
66
+ return {
67
+ value: defaultValue,
68
+ errorCode,
69
+ errorMessage,
70
+ reason: 'ERROR',
71
+ };
72
+ }