@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.
Files changed (41) hide show
  1. package/README.md +88 -36
  2. package/dist/client/index.d.ts +17 -22
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +26 -41
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/api.d.ts +4 -0
  7. package/dist/component/_generated/api.d.ts.map +1 -1
  8. package/dist/component/_generated/api.js.map +1 -1
  9. package/dist/component/_generated/component.d.ts +1 -21
  10. package/dist/component/_generated/component.d.ts.map +1 -1
  11. package/dist/component/_generated/server.d.ts +11 -0
  12. package/dist/component/_generated/server.d.ts.map +1 -1
  13. package/dist/component/_generated/server.js +1 -0
  14. package/dist/component/_generated/server.js.map +1 -1
  15. package/dist/component/convex.config.d.ts +18 -1
  16. package/dist/component/convex.config.d.ts.map +1 -1
  17. package/dist/component/convex.config.js +21 -1
  18. package/dist/component/convex.config.js.map +1 -1
  19. package/dist/component/crons.d.ts +5 -0
  20. package/dist/component/crons.d.ts.map +1 -0
  21. package/dist/component/crons.js +26 -0
  22. package/dist/component/crons.js.map +1 -0
  23. package/dist/component/lib.d.ts +11 -35
  24. package/dist/component/lib.d.ts.map +1 -1
  25. package/dist/component/lib.js +84 -63
  26. package/dist/component/lib.js.map +1 -1
  27. package/dist/component/version.d.ts +1 -1
  28. package/dist/component/version.d.ts.map +1 -1
  29. package/dist/component/version.js +1 -1
  30. package/dist/component/version.js.map +1 -1
  31. package/package.json +7 -6
  32. package/src/client/index.test.ts +85 -63
  33. package/src/client/index.ts +35 -60
  34. package/src/component/_generated/api.ts +4 -0
  35. package/src/component/_generated/component.ts +3 -27
  36. package/src/component/_generated/server.ts +11 -0
  37. package/src/component/convex.config.ts +21 -1
  38. package/src/component/crons.test.ts +96 -0
  39. package/src/component/crons.ts +36 -0
  40. package/src/component/lib.ts +80 -62
  41. 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": "1.0.10",
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
- "@posthog/core": "1.29.12",
52
- "posthog-node": "5.35.5"
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.31.7",
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.31.7"
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
- "build:codegen": "npx convex codegen --component-dir ./src/component && pnpm build",
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",
@@ -12,41 +12,25 @@ function mockSchedulerCtx() {
12
12
  }
13
13
 
14
14
  describe('PostHog client', () => {
15
- test('constructor uses defaults from env', async () => {
16
- process.env.POSTHOG_API_KEY = ' test-key\n'
17
- process.env.POSTHOG_HOST = ' https://test.posthog.com\t '
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 explicit options', () => {
20
+ test('constructor accepts identify and beforeSend callbacks', () => {
37
21
  const posthog = new PostHog({} as never, {
38
- apiKey: 'explicit-key',
39
- host: 'https://custom.posthog.com',
22
+ identify: async () => null,
23
+ beforeSend: (event) => event,
40
24
  })
41
25
  expect(posthog).toBeInstanceOf(PostHog)
42
26
  })
43
27
 
44
- test('trims apiKey and host before scheduling events', async () => {
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.apiKey).toBe('explicit-key')
59
- expect(args.host).toBe('https://custom.posthog.com/')
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, { apiKey: 'test' })
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, { apiKey: 'test' })
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, { apiKey: 'key' })
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, { apiKey: 'key' })
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, { apiKey: 'key' })
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, { apiKey: 'key' })
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, { apiKey: 'key' })
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, { apiKey: 'key' })
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, { apiKey: 'key' })
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, { apiKey: 'key' })
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, { apiKey: 'key' })
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, { apiKey: 'key' })
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, { apiKey: 'key' })
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, { apiKey: 'key' })
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, { apiKey: 'key' })
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, { apiKey: 'key' })
731
+ const posthog = new PostHog(component as never)
710
732
  const ctx = mockSchedulerCtx()
711
733
 
712
734
  await posthog.capture(ctx as never, {
@@ -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
- /** Context with runAction — available in actions. Used by `refreshFlagDefinitions`. */
81
- type RunActionCtx = { runAction: (reference: any, args: any) => Promise<any> }
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. Must be called from an action
116
- * context (typically a cron handler) the component fetches `/flags/definitions` and writes
117
- * the result to its singleton table. Returns the component's status object so callers can log
118
- * misconfiguration without throwing.
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 refreshFlagDefinitions(ctx: RunActionCtx): Promise<unknown> {
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
- data: string
154
- fetchedAt: number
130
+ localEvalConfigured: boolean
131
+ data: string | null
132
+ fetchedAt: number | null
155
133
  etag?: string
156
- } | null
157
- if (!row) return null
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
  /**