@posthog/convex 1.0.10 → 2.0.1
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 +88 -36
- package/dist/client/index.d.ts +17 -22
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +26 -41
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +4 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +1 -21
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +11 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js +1 -0
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/convex.config.d.ts +18 -1
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +21 -1
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/crons.d.ts +5 -0
- package/dist/component/crons.d.ts.map +1 -0
- package/dist/component/crons.js +26 -0
- package/dist/component/crons.js.map +1 -0
- package/dist/component/lib.d.ts +11 -35
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +84 -63
- package/dist/component/lib.js.map +1 -1
- package/dist/component/version.d.ts +1 -1
- package/dist/component/version.d.ts.map +1 -1
- package/dist/component/version.js +1 -1
- package/dist/component/version.js.map +1 -1
- package/package.json +7 -6
- package/src/client/index.test.ts +85 -63
- package/src/client/index.ts +35 -60
- package/src/component/_generated/api.ts +4 -0
- package/src/component/_generated/component.ts +3 -27
- package/src/component/_generated/server.ts +11 -0
- package/src/component/convex.config.ts +21 -1
- package/src/component/crons.test.ts +96 -0
- package/src/component/crons.ts +36 -0
- package/src/component/lib.ts +80 -62
- package/src/component/version.ts +1 -1
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"url": "https://github.com/PostHog/posthog-js/issues"
|
|
12
12
|
},
|
|
13
13
|
"author": "PostHog Inc.",
|
|
14
|
-
"version": "
|
|
14
|
+
"version": "2.0.1",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"convex",
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
"types": "./dist/client/index.d.ts",
|
|
49
49
|
"module": "./dist/client/index.js",
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"
|
|
52
|
-
"posthog
|
|
51
|
+
"posthog-node": "5.35.6",
|
|
52
|
+
"@posthog/core": "1.29.13"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@edge-runtime/vm": "^5.0.0",
|
|
56
56
|
"@jest/globals": "^29.7.0",
|
|
57
57
|
"@types/node": "^24.10.12",
|
|
58
|
-
"convex": "1.
|
|
58
|
+
"convex": "1.39.1",
|
|
59
59
|
"convex-test": "0.0.41",
|
|
60
60
|
"globals": "^17.3.0",
|
|
61
61
|
"jest": "29.7.0",
|
|
@@ -64,12 +64,13 @@
|
|
|
64
64
|
"vite": "7.3.1"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"convex": "^1.
|
|
67
|
+
"convex": "^1.39.0"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"prebuild": "node -p \"'export const version = \\'' + require('./package.json').version + '\\''\" > src/component/version.ts",
|
|
71
71
|
"build": "tsc --project ./tsconfig.build.json",
|
|
72
|
-
"
|
|
72
|
+
"setup:codegen": "test -f .env.local || npx convex dev --once --configure new --dev-deployment local --project posthog-convex-component --typecheck disable || true",
|
|
73
|
+
"build:codegen": "pnpm run setup:codegen && npx convex codegen --component-dir ./src/component --typecheck disable && pnpm build",
|
|
73
74
|
"clean": "rm -rf dist *.tsbuildinfo && pnpm build:codegen",
|
|
74
75
|
"typecheck": "tsc --noEmit",
|
|
75
76
|
"lint": "eslint src",
|
package/src/client/index.test.ts
CHANGED
|
@@ -12,41 +12,25 @@ function mockSchedulerCtx() {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
describe('PostHog client', () => {
|
|
15
|
-
test('constructor
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const component = { lib: { capture: 'capture_ref' } }
|
|
20
|
-
const posthog = new PostHog(component as never)
|
|
21
|
-
const ctx = mockSchedulerCtx()
|
|
22
|
-
|
|
23
|
-
await posthog.capture(ctx as never, {
|
|
24
|
-
distinctId: 'user-1',
|
|
25
|
-
event: 'test-event',
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
const [, , args] = ctx.scheduler.runAfter.mock.calls[0]
|
|
29
|
-
expect(args.apiKey).toBe('test-key')
|
|
30
|
-
expect(args.host).toBe('https://test.posthog.com')
|
|
31
|
-
|
|
32
|
-
delete process.env.POSTHOG_API_KEY
|
|
33
|
-
delete process.env.POSTHOG_HOST
|
|
15
|
+
test('constructor accepts no options', () => {
|
|
16
|
+
const posthog = new PostHog({} as never)
|
|
17
|
+
expect(posthog).toBeInstanceOf(PostHog)
|
|
34
18
|
})
|
|
35
19
|
|
|
36
|
-
test('constructor accepts
|
|
20
|
+
test('constructor accepts identify and beforeSend callbacks', () => {
|
|
37
21
|
const posthog = new PostHog({} as never, {
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
identify: async () => null,
|
|
23
|
+
beforeSend: (event) => event,
|
|
40
24
|
})
|
|
41
25
|
expect(posthog).toBeInstanceOf(PostHog)
|
|
42
26
|
})
|
|
43
27
|
|
|
44
|
-
test('
|
|
28
|
+
test('does not forward credentials to component calls (env-driven config)', async () => {
|
|
29
|
+
// Credentials live on the component as env vars (POSTHOG_PROJECT_TOKEN, POSTHOG_HOST,
|
|
30
|
+
// POSTHOG_PERSONAL_API_KEY) declared in convex.config.ts and read inside each action.
|
|
31
|
+
// The client must not plumb them through every call site.
|
|
45
32
|
const component = { lib: { capture: 'capture_ref' } }
|
|
46
|
-
const posthog = new PostHog(component as never
|
|
47
|
-
apiKey: ' explicit-key\n',
|
|
48
|
-
host: ' https://custom.posthog.com/\t ',
|
|
49
|
-
})
|
|
33
|
+
const posthog = new PostHog(component as never)
|
|
50
34
|
const ctx = mockSchedulerCtx()
|
|
51
35
|
|
|
52
36
|
await posthog.capture(ctx as never, {
|
|
@@ -55,12 +39,13 @@ describe('PostHog client', () => {
|
|
|
55
39
|
})
|
|
56
40
|
|
|
57
41
|
const [, , args] = ctx.scheduler.runAfter.mock.calls[0]
|
|
58
|
-
expect(args
|
|
59
|
-
expect(args
|
|
42
|
+
expect(args).not.toHaveProperty('apiKey')
|
|
43
|
+
expect(args).not.toHaveProperty('host')
|
|
44
|
+
expect(args).not.toHaveProperty('personalApiKey')
|
|
60
45
|
})
|
|
61
46
|
|
|
62
47
|
test('exposes capture, identify, groupIdentify, alias, captureException methods', () => {
|
|
63
|
-
const posthog = new PostHog({} as never
|
|
48
|
+
const posthog = new PostHog({} as never)
|
|
64
49
|
|
|
65
50
|
expect(typeof posthog.capture).toBe('function')
|
|
66
51
|
expect(typeof posthog.identify).toBe('function')
|
|
@@ -70,7 +55,7 @@ describe('PostHog client', () => {
|
|
|
70
55
|
})
|
|
71
56
|
|
|
72
57
|
test('exposes feature flag methods', () => {
|
|
73
|
-
const posthog = new PostHog({} as never
|
|
58
|
+
const posthog = new PostHog({} as never)
|
|
74
59
|
|
|
75
60
|
expect(typeof posthog.getFeatureFlag).toBe('function')
|
|
76
61
|
expect(typeof posthog.isFeatureEnabled).toBe('function')
|
|
@@ -80,6 +65,21 @@ describe('PostHog client', () => {
|
|
|
80
65
|
expect(typeof posthog.getAllFlagsAndPayloads).toBe('function')
|
|
81
66
|
})
|
|
82
67
|
|
|
68
|
+
test('exposes reloadFeatureFlags method (parity with posthog-node)', () => {
|
|
69
|
+
const posthog = new PostHog({} as never)
|
|
70
|
+
expect(typeof posthog.reloadFeatureFlags).toBe('function')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('reloadFeatureFlags forwards to the component refresh action with no args', async () => {
|
|
74
|
+
const component = { lib: { refreshFlagDefinitions: 'refresh_ref' } }
|
|
75
|
+
const posthog = new PostHog(component as never)
|
|
76
|
+
const ctx = { runAction: jest.fn(async () => ({ status: 'updated' })) }
|
|
77
|
+
|
|
78
|
+
await posthog.reloadFeatureFlags(ctx as never)
|
|
79
|
+
|
|
80
|
+
expect(ctx.runAction).toHaveBeenCalledWith('refresh_ref', {})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
83
|
test('getFeatureFlagPayload with matchValue does not require a distinctId', async () => {
|
|
84
84
|
// The matchValue path is a pure key+value payload lookup; resolving a distinctId would
|
|
85
85
|
// force callers to configure an identify callback or pass an ID they don't have.
|
|
@@ -105,9 +105,9 @@ describe('PostHog client', () => {
|
|
|
105
105
|
cohorts: {},
|
|
106
106
|
})
|
|
107
107
|
const component = { lib: { getFlagDefinitions: 'getFlagDefinitions_ref' } }
|
|
108
|
-
const posthog = new PostHog(component as never
|
|
108
|
+
const posthog = new PostHog(component as never)
|
|
109
109
|
const ctx = {
|
|
110
|
-
runQuery: jest.fn(async () => ({ data: definitions, fetchedAt: Date.now() })),
|
|
110
|
+
runQuery: jest.fn(async () => ({ localEvalConfigured: true, data: definitions, fetchedAt: Date.now() })),
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const payload = await posthog.getFeatureFlagPayload(ctx as never, { key: 'flag', matchValue: 'red' })
|
|
@@ -115,6 +115,44 @@ describe('PostHog client', () => {
|
|
|
115
115
|
})
|
|
116
116
|
})
|
|
117
117
|
|
|
118
|
+
describe('local-eval configuration', () => {
|
|
119
|
+
// When `POSTHOG_PERSONAL_API_KEY` isn't set on the component the cron never runs and local eval
|
|
120
|
+
// can never produce a verdict. The client throws so callers don't silently get `undefined` and
|
|
121
|
+
// wonder why their flag rollouts aren't taking effect.
|
|
122
|
+
test('throws when local eval is not configured (no PAK)', async () => {
|
|
123
|
+
const component = { lib: { getFlagDefinitions: 'getFlagDefinitions_ref' } }
|
|
124
|
+
const posthog = new PostHog(component as never)
|
|
125
|
+
const ctx = {
|
|
126
|
+
runQuery: jest.fn(async () => ({
|
|
127
|
+
localEvalConfigured: false,
|
|
128
|
+
data: null,
|
|
129
|
+
fetchedAt: null,
|
|
130
|
+
})),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await expect(posthog.getFeatureFlag(ctx as never, { key: 'my-flag', distinctId: 'u1' })).rejects.toThrow(
|
|
134
|
+
'local feature flag evaluation is not configured'
|
|
135
|
+
)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// PAK *is* set but the first cron tick hasn't landed yet — graceful degradation rather than
|
|
139
|
+
// throwing, so callers' fallback paths still work during the brief warm-up window.
|
|
140
|
+
test('returns undefined when PAK is configured but definitions have not been cached yet', async () => {
|
|
141
|
+
const component = { lib: { getFlagDefinitions: 'getFlagDefinitions_ref' } }
|
|
142
|
+
const posthog = new PostHog(component as never)
|
|
143
|
+
const ctx = {
|
|
144
|
+
runQuery: jest.fn(async () => ({
|
|
145
|
+
localEvalConfigured: true,
|
|
146
|
+
data: null,
|
|
147
|
+
fetchedAt: null,
|
|
148
|
+
})),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const value = await posthog.getFeatureFlag(ctx as never, { key: 'my-flag', distinctId: 'u1' })
|
|
152
|
+
expect(value).toBeUndefined()
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
118
156
|
describe('normalizeError', () => {
|
|
119
157
|
test('extracts message, stack, and name from Error instances', () => {
|
|
120
158
|
const error = new Error('test error')
|
|
@@ -170,7 +208,7 @@ describe('captureException', () => {
|
|
|
170
208
|
const component = {
|
|
171
209
|
lib: { captureException: 'captureException_ref' },
|
|
172
210
|
}
|
|
173
|
-
const posthog = new PostHog(component as never
|
|
211
|
+
const posthog = new PostHog(component as never)
|
|
174
212
|
const ctx = mockSchedulerCtx()
|
|
175
213
|
|
|
176
214
|
await posthog.captureException(ctx as never, {
|
|
@@ -194,7 +232,7 @@ describe('captureException', () => {
|
|
|
194
232
|
const component = {
|
|
195
233
|
lib: { captureException: 'captureException_ref' },
|
|
196
234
|
}
|
|
197
|
-
const posthog = new PostHog(component as never
|
|
235
|
+
const posthog = new PostHog(component as never)
|
|
198
236
|
const ctx = mockSchedulerCtx()
|
|
199
237
|
|
|
200
238
|
await posthog.captureException(ctx as never, {
|
|
@@ -211,7 +249,7 @@ describe('captureException', () => {
|
|
|
211
249
|
describe('$-prefixed property serialization', () => {
|
|
212
250
|
test('serializes properties with $-prefixed keys as JSON strings', async () => {
|
|
213
251
|
const component = { lib: { capture: 'capture_ref' } }
|
|
214
|
-
const posthog = new PostHog(component as never
|
|
252
|
+
const posthog = new PostHog(component as never)
|
|
215
253
|
const ctx = mockSchedulerCtx()
|
|
216
254
|
|
|
217
255
|
await posthog.capture(ctx as never, {
|
|
@@ -237,7 +275,7 @@ describe('$-prefixed property serialization', () => {
|
|
|
237
275
|
|
|
238
276
|
test('serializes identify properties with $set and $set_once', async () => {
|
|
239
277
|
const component = { lib: { identify: 'identify_ref' } }
|
|
240
|
-
const posthog = new PostHog(component as never
|
|
278
|
+
const posthog = new PostHog(component as never)
|
|
241
279
|
const ctx = mockSchedulerCtx()
|
|
242
280
|
|
|
243
281
|
await posthog.identify(ctx as never, {
|
|
@@ -258,7 +296,7 @@ describe('$-prefixed property serialization', () => {
|
|
|
258
296
|
|
|
259
297
|
test('passes undefined when properties are not provided', async () => {
|
|
260
298
|
const component = { lib: { capture: 'capture_ref' } }
|
|
261
|
-
const posthog = new PostHog(component as never
|
|
299
|
+
const posthog = new PostHog(component as never)
|
|
262
300
|
const ctx = mockSchedulerCtx()
|
|
263
301
|
|
|
264
302
|
await posthog.capture(ctx as never, {
|
|
@@ -272,7 +310,7 @@ describe('$-prefixed property serialization', () => {
|
|
|
272
310
|
|
|
273
311
|
test('preserves nested objects and arrays through serialization', async () => {
|
|
274
312
|
const component = { lib: { capture: 'capture_ref' } }
|
|
275
|
-
const posthog = new PostHog(component as never
|
|
313
|
+
const posthog = new PostHog(component as never)
|
|
276
314
|
const ctx = mockSchedulerCtx()
|
|
277
315
|
|
|
278
316
|
const properties = {
|
|
@@ -296,7 +334,7 @@ describe('$-prefixed property serialization', () => {
|
|
|
296
334
|
|
|
297
335
|
test('serializes groups with string and number values', async () => {
|
|
298
336
|
const component = { lib: { capture: 'capture_ref' } }
|
|
299
|
-
const posthog = new PostHog(component as never
|
|
337
|
+
const posthog = new PostHog(component as never)
|
|
300
338
|
const ctx = mockSchedulerCtx()
|
|
301
339
|
|
|
302
340
|
await posthog.capture(ctx as never, {
|
|
@@ -312,7 +350,7 @@ describe('$-prefixed property serialization', () => {
|
|
|
312
350
|
|
|
313
351
|
test('serializes captureException additionalProperties', async () => {
|
|
314
352
|
const component = { lib: { captureException: 'captureException_ref' } }
|
|
315
|
-
const posthog = new PostHog(component as never
|
|
353
|
+
const posthog = new PostHog(component as never)
|
|
316
354
|
const ctx = mockSchedulerCtx()
|
|
317
355
|
|
|
318
356
|
await posthog.captureException(ctx as never, {
|
|
@@ -331,7 +369,7 @@ describe('$-prefixed property serialization', () => {
|
|
|
331
369
|
|
|
332
370
|
test('handles empty objects', async () => {
|
|
333
371
|
const component = { lib: { capture: 'capture_ref' } }
|
|
334
|
-
const posthog = new PostHog(component as never
|
|
372
|
+
const posthog = new PostHog(component as never)
|
|
335
373
|
const ctx = mockSchedulerCtx()
|
|
336
374
|
|
|
337
375
|
await posthog.capture(ctx as never, {
|
|
@@ -350,7 +388,7 @@ describe('$-prefixed property serialization', () => {
|
|
|
350
388
|
describe('beforeSend', () => {
|
|
351
389
|
test('allows events through when no beforeSend is configured', async () => {
|
|
352
390
|
const component = { lib: { capture: 'capture_ref' } }
|
|
353
|
-
const posthog = new PostHog(component as never
|
|
391
|
+
const posthog = new PostHog(component as never)
|
|
354
392
|
const ctx = mockSchedulerCtx()
|
|
355
393
|
|
|
356
394
|
await posthog.capture(ctx as never, {
|
|
@@ -365,7 +403,6 @@ describe('beforeSend', () => {
|
|
|
365
403
|
const component = { lib: { capture: 'capture_ref' } }
|
|
366
404
|
const beforeSend: BeforeSendFn = () => null
|
|
367
405
|
const posthog = new PostHog(component as never, {
|
|
368
|
-
apiKey: 'key',
|
|
369
406
|
beforeSend,
|
|
370
407
|
})
|
|
371
408
|
const ctx = mockSchedulerCtx()
|
|
@@ -385,7 +422,6 @@ describe('beforeSend', () => {
|
|
|
385
422
|
properties: { ...event.properties, injected: true },
|
|
386
423
|
})
|
|
387
424
|
const posthog = new PostHog(component as never, {
|
|
388
|
-
apiKey: 'key',
|
|
389
425
|
beforeSend,
|
|
390
426
|
})
|
|
391
427
|
const ctx = mockSchedulerCtx()
|
|
@@ -411,7 +447,6 @@ describe('beforeSend', () => {
|
|
|
411
447
|
properties: { ...event.properties, second: true },
|
|
412
448
|
})
|
|
413
449
|
const posthog = new PostHog(component as never, {
|
|
414
|
-
apiKey: 'key',
|
|
415
450
|
beforeSend: [fn1, fn2],
|
|
416
451
|
})
|
|
417
452
|
const ctx = mockSchedulerCtx()
|
|
@@ -430,7 +465,6 @@ describe('beforeSend', () => {
|
|
|
430
465
|
const fn1: BeforeSendFn = () => null
|
|
431
466
|
const fn2: BeforeSendFn = jest.fn((event) => event)
|
|
432
467
|
const posthog = new PostHog(component as never, {
|
|
433
|
-
apiKey: 'key',
|
|
434
468
|
beforeSend: [fn1, fn2],
|
|
435
469
|
})
|
|
436
470
|
const ctx = mockSchedulerCtx()
|
|
@@ -451,7 +485,6 @@ describe('beforeSend', () => {
|
|
|
451
485
|
return null
|
|
452
486
|
}
|
|
453
487
|
const posthog = new PostHog(component as never, {
|
|
454
|
-
apiKey: 'key',
|
|
455
488
|
beforeSend,
|
|
456
489
|
})
|
|
457
490
|
const ctx = mockSchedulerCtx()
|
|
@@ -470,7 +503,6 @@ describe('beforeSend', () => {
|
|
|
470
503
|
return event
|
|
471
504
|
}
|
|
472
505
|
const posthog = new PostHog(component as never, {
|
|
473
|
-
apiKey: 'key',
|
|
474
506
|
beforeSend,
|
|
475
507
|
})
|
|
476
508
|
const ctx = mockSchedulerCtx()
|
|
@@ -492,7 +524,6 @@ describe('beforeSend', () => {
|
|
|
492
524
|
return null
|
|
493
525
|
}
|
|
494
526
|
const posthog = new PostHog(component as never, {
|
|
495
|
-
apiKey: 'key',
|
|
496
527
|
beforeSend,
|
|
497
528
|
})
|
|
498
529
|
const ctx = mockSchedulerCtx()
|
|
@@ -513,7 +544,6 @@ describe('beforeSend', () => {
|
|
|
513
544
|
return event
|
|
514
545
|
}
|
|
515
546
|
const posthog = new PostHog(component as never, {
|
|
516
|
-
apiKey: 'key',
|
|
517
547
|
beforeSend,
|
|
518
548
|
})
|
|
519
549
|
const ctx = mockSchedulerCtx()
|
|
@@ -535,7 +565,6 @@ describe('identify callback', () => {
|
|
|
535
565
|
test('uses identify callback result for capture', async () => {
|
|
536
566
|
const component = { lib: { capture: 'capture_ref' } }
|
|
537
567
|
const posthog = new PostHog(component as never, {
|
|
538
|
-
apiKey: 'key',
|
|
539
568
|
identify: identifyReturning('auth-user-1'),
|
|
540
569
|
})
|
|
541
570
|
const ctx = mockSchedulerCtx()
|
|
@@ -549,7 +578,6 @@ describe('identify callback', () => {
|
|
|
549
578
|
test('falls back to explicit distinctId when identify returns null', async () => {
|
|
550
579
|
const component = { lib: { capture: 'capture_ref' } }
|
|
551
580
|
const posthog = new PostHog(component as never, {
|
|
552
|
-
apiKey: 'key',
|
|
553
581
|
identify: identifyReturningNull,
|
|
554
582
|
})
|
|
555
583
|
const ctx = mockSchedulerCtx()
|
|
@@ -566,7 +594,6 @@ describe('identify callback', () => {
|
|
|
566
594
|
test('throws when neither identify nor explicit distinctId resolves', async () => {
|
|
567
595
|
const component = { lib: { capture: 'capture_ref' } }
|
|
568
596
|
const posthog = new PostHog(component as never, {
|
|
569
|
-
apiKey: 'key',
|
|
570
597
|
identify: identifyReturningNull,
|
|
571
598
|
})
|
|
572
599
|
const ctx = mockSchedulerCtx()
|
|
@@ -576,7 +603,7 @@ describe('identify callback', () => {
|
|
|
576
603
|
|
|
577
604
|
test('throws when no identify configured and no explicit distinctId', async () => {
|
|
578
605
|
const component = { lib: { capture: 'capture_ref' } }
|
|
579
|
-
const posthog = new PostHog(component as never
|
|
606
|
+
const posthog = new PostHog(component as never)
|
|
580
607
|
const ctx = mockSchedulerCtx()
|
|
581
608
|
|
|
582
609
|
await expect(posthog.capture(ctx as never, { event: 'test_event' })).rejects.toThrow('Could not resolve distinctId')
|
|
@@ -585,7 +612,6 @@ describe('identify callback', () => {
|
|
|
585
612
|
test('identify callback takes precedence over explicit distinctId', async () => {
|
|
586
613
|
const component = { lib: { capture: 'capture_ref' } }
|
|
587
614
|
const posthog = new PostHog(component as never, {
|
|
588
|
-
apiKey: 'key',
|
|
589
615
|
identify: identifyReturning('auth-user'),
|
|
590
616
|
})
|
|
591
617
|
const ctx = mockSchedulerCtx()
|
|
@@ -603,7 +629,6 @@ describe('identify callback', () => {
|
|
|
603
629
|
const component = { lib: { capture: 'capture_ref' } }
|
|
604
630
|
const identify = jest.fn(async () => ({ distinctId: 'resolved' }))
|
|
605
631
|
const posthog = new PostHog(component as never, {
|
|
606
|
-
apiKey: 'key',
|
|
607
632
|
identify,
|
|
608
633
|
})
|
|
609
634
|
const ctx = mockSchedulerCtx()
|
|
@@ -616,7 +641,6 @@ describe('identify callback', () => {
|
|
|
616
641
|
test('works with identify method', async () => {
|
|
617
642
|
const component = { lib: { identify: 'identify_ref' } }
|
|
618
643
|
const posthog = new PostHog(component as never, {
|
|
619
|
-
apiKey: 'key',
|
|
620
644
|
identify: identifyReturning('auth-user'),
|
|
621
645
|
})
|
|
622
646
|
const ctx = mockSchedulerCtx()
|
|
@@ -630,7 +654,6 @@ describe('identify callback', () => {
|
|
|
630
654
|
test('works with alias method', async () => {
|
|
631
655
|
const component = { lib: { alias: 'alias_ref' } }
|
|
632
656
|
const posthog = new PostHog(component as never, {
|
|
633
|
-
apiKey: 'key',
|
|
634
657
|
identify: identifyReturning('auth-user'),
|
|
635
658
|
})
|
|
636
659
|
const ctx = mockSchedulerCtx()
|
|
@@ -645,7 +668,7 @@ describe('identify callback', () => {
|
|
|
645
668
|
const component = {
|
|
646
669
|
lib: { captureException: 'captureException_ref' },
|
|
647
670
|
}
|
|
648
|
-
const posthog = new PostHog(component as never
|
|
671
|
+
const posthog = new PostHog(component as never)
|
|
649
672
|
const ctx = mockSchedulerCtx()
|
|
650
673
|
|
|
651
674
|
await posthog.captureException(ctx as never, {
|
|
@@ -662,7 +685,6 @@ describe('identify callback', () => {
|
|
|
662
685
|
lib: { captureException: 'captureException_ref' },
|
|
663
686
|
}
|
|
664
687
|
const posthog = new PostHog(component as never, {
|
|
665
|
-
apiKey: 'key',
|
|
666
688
|
identify: identifyReturning('auth-user'),
|
|
667
689
|
})
|
|
668
690
|
const ctx = mockSchedulerCtx()
|
|
@@ -682,11 +704,11 @@ describe('identify callback', () => {
|
|
|
682
704
|
try {
|
|
683
705
|
const component = { lib: { getFlagDefinitions: 'getFlagDefinitions_ref' } }
|
|
684
706
|
const posthog = new PostHog(component as never, {
|
|
685
|
-
apiKey: 'key',
|
|
686
707
|
identify: identifyReturning('auth-user'),
|
|
687
708
|
})
|
|
688
709
|
// Stub real-looking flag definitions so `loadEvaluator` returns an instance.
|
|
689
710
|
const definitions = {
|
|
711
|
+
localEvalConfigured: true,
|
|
690
712
|
data: JSON.stringify({ flags: [], groupTypeMapping: {}, cohorts: {} }),
|
|
691
713
|
fetchedAt: Date.now(),
|
|
692
714
|
}
|
|
@@ -706,7 +728,7 @@ describe('identify callback', () => {
|
|
|
706
728
|
|
|
707
729
|
test('explicit distinctId still works without identify callback', async () => {
|
|
708
730
|
const component = { lib: { capture: 'capture_ref' } }
|
|
709
|
-
const posthog = new PostHog(component as never
|
|
731
|
+
const posthog = new PostHog(component as never)
|
|
710
732
|
const ctx = mockSchedulerCtx()
|
|
711
733
|
|
|
712
734
|
await posthog.capture(ctx as never, {
|
package/src/client/index.ts
CHANGED
|
@@ -14,6 +14,9 @@ type SchedulerCtx = { scheduler: Scheduler }
|
|
|
14
14
|
/** Context with runQuery — available in queries, mutations, and actions. */
|
|
15
15
|
type RunQueryCtx = { runQuery: (reference: any, args: any) => Promise<any> }
|
|
16
16
|
|
|
17
|
+
/** Context with runAction — available in actions. Used by remote flag evaluation methods. */
|
|
18
|
+
type RunActionCtx = { runAction: (reference: any, args: any) => Promise<any> }
|
|
19
|
+
|
|
17
20
|
type FeatureFlagOptions = {
|
|
18
21
|
groups?: Record<string, string>
|
|
19
22
|
personProperties?: Record<string, any>
|
|
@@ -23,17 +26,6 @@ type FeatureFlagOptions = {
|
|
|
23
26
|
|
|
24
27
|
type AllFlagsOptions = FeatureFlagOptions & { flagKeys?: string[] }
|
|
25
28
|
|
|
26
|
-
const DEFAULT_HOST = 'https://us.i.posthog.com'
|
|
27
|
-
|
|
28
|
-
function normalizeApiKey(value?: unknown): string {
|
|
29
|
-
return typeof value === 'string' ? value.trim() : ''
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function normalizeHost(value?: unknown): string {
|
|
33
|
-
const normalizedValue = typeof value === 'string' ? value.trim() : ''
|
|
34
|
-
return normalizedValue || DEFAULT_HOST
|
|
35
|
-
}
|
|
36
|
-
|
|
37
29
|
export type { FeatureFlagResult, FeatureFlagValue, JsonType }
|
|
38
30
|
|
|
39
31
|
export type PostHogEvent = {
|
|
@@ -77,52 +69,37 @@ export function normalizeError(error: unknown): {
|
|
|
77
69
|
return { message: String(error) }
|
|
78
70
|
}
|
|
79
71
|
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Client-side wrapper around the PostHog Convex component.
|
|
74
|
+
*
|
|
75
|
+
* Credentials (`POSTHOG_PROJECT_TOKEN`, `POSTHOG_HOST`, `POSTHOG_PERSONAL_API_KEY`) are declared on the
|
|
76
|
+
* component in `convex.config.ts` and read directly inside the component's actions — they don't
|
|
77
|
+
* need to be plumbed through every call site. Configure callbacks (identify, beforeSend) on the
|
|
78
|
+
* client; everything else lives in env vars.
|
|
79
|
+
*/
|
|
83
80
|
export class PostHog {
|
|
84
|
-
private apiKey: string
|
|
85
|
-
private personalApiKey: string
|
|
86
|
-
private host: string
|
|
87
81
|
private beforeSend?: BeforeSendFn | BeforeSendFn[]
|
|
88
82
|
private identifyFn?: IdentifyFn
|
|
89
83
|
|
|
90
84
|
constructor(
|
|
91
85
|
public component: ComponentApi,
|
|
92
86
|
options?: {
|
|
93
|
-
apiKey?: string
|
|
94
|
-
/**
|
|
95
|
-
* Either a [feature flags secure API key](https://posthog.com/docs/feature-flags/local-evaluation#step-1-find-your-feature-flags-secure-api-key)
|
|
96
|
-
* (`phs_…`, recommended) or a personal API key (`phx_…`) with feature-flag read access.
|
|
97
|
-
* Required for local feature flag evaluation; defaults to `process.env.POSTHOG_PERSONAL_API_KEY`.
|
|
98
|
-
* The key is captured at construction time and forwarded to the component whenever you call
|
|
99
|
-
* `refreshFlagDefinitions(ctx)`.
|
|
100
|
-
*/
|
|
101
|
-
personalApiKey?: string
|
|
102
|
-
host?: string
|
|
103
87
|
beforeSend?: BeforeSendFn | BeforeSendFn[]
|
|
104
88
|
identify?: IdentifyFn
|
|
105
89
|
}
|
|
106
90
|
) {
|
|
107
|
-
this.apiKey = normalizeApiKey(options?.apiKey ?? process.env.POSTHOG_API_KEY)
|
|
108
|
-
this.personalApiKey = normalizeApiKey(options?.personalApiKey ?? process.env.POSTHOG_PERSONAL_API_KEY)
|
|
109
|
-
this.host = normalizeHost(options?.host ?? process.env.POSTHOG_HOST)
|
|
110
91
|
this.beforeSend = options?.beforeSend
|
|
111
92
|
this.identifyFn = options?.identify
|
|
112
93
|
}
|
|
113
94
|
|
|
114
95
|
/**
|
|
115
|
-
* Trigger a refresh of the cached feature flag definitions.
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
96
|
+
* Trigger a one-off refresh of the cached feature flag definitions. Named for parity with
|
|
97
|
+
* `posthog-node`'s `reloadFeatureFlags()`. The component already refreshes on a cron when
|
|
98
|
+
* `POSTHOG_PERSONAL_API_KEY` is set, so call this only when you need an immediate refresh
|
|
99
|
+
* (e.g. after creating a flag in development). Requires an action context.
|
|
119
100
|
*/
|
|
120
|
-
async
|
|
121
|
-
return await ctx.runAction(this.component.lib.refreshFlagDefinitions, {
|
|
122
|
-
apiKey: this.apiKey,
|
|
123
|
-
personalApiKey: this.personalApiKey,
|
|
124
|
-
host: this.host,
|
|
125
|
-
})
|
|
101
|
+
async reloadFeatureFlags(ctx: RunActionCtx): Promise<unknown> {
|
|
102
|
+
return await ctx.runAction(this.component.lib.refreshFlagDefinitions, {})
|
|
126
103
|
}
|
|
127
104
|
|
|
128
105
|
private async resolveDistinctId(ctx: unknown, argsDistinctId?: string): Promise<string> {
|
|
@@ -150,11 +127,25 @@ export class PostHog {
|
|
|
150
127
|
|
|
151
128
|
private async loadEvaluator(ctx: RunQueryCtx): Promise<LocalFeatureFlagEvaluator | null> {
|
|
152
129
|
const row = (await ctx.runQuery(this.component.lib.getFlagDefinitions, {})) as {
|
|
153
|
-
|
|
154
|
-
|
|
130
|
+
localEvalConfigured: boolean
|
|
131
|
+
data: string | null
|
|
132
|
+
fetchedAt: number | null
|
|
155
133
|
etag?: string
|
|
156
|
-
}
|
|
157
|
-
if (!row)
|
|
134
|
+
}
|
|
135
|
+
if (!row.localEvalConfigured) {
|
|
136
|
+
// Loud failure rather than silent `undefined`: a caller invoking a local-eval method
|
|
137
|
+
// without `POSTHOG_PERSONAL_API_KEY` configured almost certainly meant to use a remote
|
|
138
|
+
// `evaluate*` method instead. Throwing tells them exactly what to do.
|
|
139
|
+
throw new Error(
|
|
140
|
+
'PostHog: local feature flag evaluation is not configured. ' +
|
|
141
|
+
'Set POSTHOG_PERSONAL_API_KEY on your Convex deployment, or call the remote ' +
|
|
142
|
+
'`evaluateFlag` / `evaluateFlagPayload` / `evaluateAllFlags` methods instead ' +
|
|
143
|
+
'(action context only).'
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
// PAK is set but the cron hasn't populated the cache yet — return null so callers fall
|
|
147
|
+
// back to their `undefined` graceful-degrade path until definitions land.
|
|
148
|
+
if (!row.data) return null
|
|
158
149
|
let parsed: FlagDefinitions
|
|
159
150
|
try {
|
|
160
151
|
parsed = JSON.parse(row.data) as FlagDefinitions
|
|
@@ -189,8 +180,6 @@ export class PostHog {
|
|
|
189
180
|
if (!result) return
|
|
190
181
|
|
|
191
182
|
await ctx.scheduler.runAfter(0, this.component.lib.capture, {
|
|
192
|
-
apiKey: this.apiKey,
|
|
193
|
-
host: this.host,
|
|
194
183
|
distinctId: result.distinctId,
|
|
195
184
|
event: result.event,
|
|
196
185
|
properties: result.properties ? JSON.stringify(result.properties) : undefined,
|
|
@@ -223,8 +212,6 @@ export class PostHog {
|
|
|
223
212
|
if (!result) return
|
|
224
213
|
|
|
225
214
|
await ctx.scheduler.runAfter(0, this.component.lib.identify, {
|
|
226
|
-
apiKey: this.apiKey,
|
|
227
|
-
host: this.host,
|
|
228
215
|
distinctId: result.distinctId,
|
|
229
216
|
properties: result.properties ? JSON.stringify(result.properties) : undefined,
|
|
230
217
|
disableGeoip: args.disableGeoip,
|
|
@@ -249,8 +236,6 @@ export class PostHog {
|
|
|
249
236
|
if (!result) return
|
|
250
237
|
|
|
251
238
|
await ctx.scheduler.runAfter(0, this.component.lib.groupIdentify, {
|
|
252
|
-
apiKey: this.apiKey,
|
|
253
|
-
host: this.host,
|
|
254
239
|
groupType: args.groupType,
|
|
255
240
|
groupKey: args.groupKey,
|
|
256
241
|
properties: result.properties ? JSON.stringify(result.properties) : undefined,
|
|
@@ -278,8 +263,6 @@ export class PostHog {
|
|
|
278
263
|
if (!result) return
|
|
279
264
|
|
|
280
265
|
await ctx.scheduler.runAfter(0, this.component.lib.alias, {
|
|
281
|
-
apiKey: this.apiKey,
|
|
282
|
-
host: this.host,
|
|
283
266
|
distinctId: result.distinctId,
|
|
284
267
|
alias: args.alias,
|
|
285
268
|
disableGeoip: args.disableGeoip,
|
|
@@ -311,8 +294,6 @@ export class PostHog {
|
|
|
311
294
|
if (!result) return
|
|
312
295
|
|
|
313
296
|
await ctx.scheduler.runAfter(0, this.component.lib.captureException, {
|
|
314
|
-
apiKey: this.apiKey,
|
|
315
|
-
host: this.host,
|
|
316
297
|
distinctId: result.distinctId || undefined,
|
|
317
298
|
errorMessage: message,
|
|
318
299
|
errorStack: stack,
|
|
@@ -469,8 +450,6 @@ export class PostHog {
|
|
|
469
450
|
): Promise<FeatureFlagValue | null> {
|
|
470
451
|
const distinctId = await this.resolveDistinctId(ctx, args.distinctId)
|
|
471
452
|
return (await ctx.runAction(this.component.lib.evaluateFlag, {
|
|
472
|
-
apiKey: this.apiKey,
|
|
473
|
-
host: this.host,
|
|
474
453
|
key: args.key,
|
|
475
454
|
distinctId,
|
|
476
455
|
groups: args.groups,
|
|
@@ -490,8 +469,6 @@ export class PostHog {
|
|
|
490
469
|
): Promise<JsonType | null> {
|
|
491
470
|
const distinctId = await this.resolveDistinctId(ctx, args.distinctId)
|
|
492
471
|
return (await ctx.runAction(this.component.lib.evaluateFlagPayload, {
|
|
493
|
-
apiKey: this.apiKey,
|
|
494
|
-
host: this.host,
|
|
495
472
|
key: args.key,
|
|
496
473
|
distinctId,
|
|
497
474
|
groups: args.groups,
|
|
@@ -515,8 +492,6 @@ export class PostHog {
|
|
|
515
492
|
}> {
|
|
516
493
|
const distinctId = await this.resolveDistinctId(ctx, args.distinctId)
|
|
517
494
|
return (await ctx.runAction(this.component.lib.evaluateAllFlags, {
|
|
518
|
-
apiKey: this.apiKey,
|
|
519
|
-
host: this.host,
|
|
520
495
|
distinctId,
|
|
521
496
|
groups: args.groups,
|
|
522
497
|
personProperties: args.personProperties,
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import type * as crons from "../crons.js";
|
|
11
12
|
import type * as lib from "../lib.js";
|
|
13
|
+
import type * as version from "../version.js";
|
|
12
14
|
|
|
13
15
|
import type {
|
|
14
16
|
ApiFromModules,
|
|
@@ -18,7 +20,9 @@ import type {
|
|
|
18
20
|
import { anyApi, componentsGeneric } from "convex/server";
|
|
19
21
|
|
|
20
22
|
const fullApi: ApiFromModules<{
|
|
23
|
+
crons: typeof crons;
|
|
21
24
|
lib: typeof lib;
|
|
25
|
+
version: typeof version;
|
|
22
26
|
}> = anyApi as any;
|
|
23
27
|
|
|
24
28
|
/**
|