@posthog/convex 1.0.10 → 2.0.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 (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 +12 -0
  20. package/dist/component/crons.d.ts.map +1 -0
  21. package/dist/component/crons.js +42 -0
  22. package/dist/component/crons.js.map +1 -0
  23. package/dist/component/lib.d.ts +18 -32
  24. package/dist/component/lib.d.ts.map +1 -1
  25. package/dist/component/lib.js +90 -60
  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 +5 -4
  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 +62 -0
  39. package/src/component/crons.ts +52 -0
  40. package/src/component/lib.ts +86 -59
  41. package/src/component/version.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/component/_generated/server.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,eAAe,CAAC;AAUvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,EAAE,YAAY,CAAC,SAAS,EAAE,QAAQ,CAAgB,CAAC;AAErE;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,YAAY,CAAC,SAAS,EAAE,UAAU,CACxC,CAAC;AAEvB;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,EAAE,eAAe,CAAC,SAAS,EAAE,QAAQ,CAAmB,CAAC;AAE9E;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,EAAE,eAAe,CAAC,SAAS,EAAE,UAAU,CAC3C,CAAC;AAE1B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,MAAM,EAAE,aAAa,CAAC,SAAS,EAAE,QAAQ,CAAiB,CAAC;AAExE;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,aAAa,CAAC,SAAS,EAAE,UAAU,CACzC,CAAC;AAExB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU,EAAE,iBAAqC,CAAC;AAE/D;;;;;;;GAOG;AACH,MAAM,MAAM,QAAQ,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;AAElD;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;AAExD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;AAE9D;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/component/_generated/server.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,eAAe,CAAC;AAUvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD;;GAEG;AACH,KAAK,GAAG,GAAG;IACT,QAAQ,CAAC,sCAAsC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpE,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,wBAAwB,EAAE,MAAM,GAAG,SAAS,CAAC;IACtD,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;CACxC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,EAAE,YAAY,CAAC,SAAS,EAAE,QAAQ,CAAgB,CAAC;AAErE;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,YAAY,CAAC,SAAS,EAAE,UAAU,CACxC,CAAC;AAEvB;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,EAAE,eAAe,CAAC,SAAS,EAAE,QAAQ,CAAmB,CAAC;AAE9E;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,EAAE,eAAe,CAAC,SAAS,EAAE,UAAU,CAC3C,CAAC;AAE1B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,MAAM,EAAE,aAAa,CAAC,SAAS,EAAE,QAAQ,CAAiB,CAAC;AAExE;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,aAAa,CAAC,SAAS,EAAE,UAAU,CACzC,CAAC;AAExB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU,EAAE,iBAAqC,CAAC;AAC/D,eAAO,MAAM,GAAG,EAAE,GAAmC,CAAC;AAEtD;;;;;;;GAOG;AACH,MAAM,MAAM,QAAQ,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;AAElD;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;AAExD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;AAE9D;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC"}
@@ -75,4 +75,5 @@ export const internalAction = internalActionGeneric;
75
75
  * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
76
76
  */
77
77
  export const httpAction = httpActionGeneric;
78
+ export const env = process.env;
78
79
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../../src/component/_generated/server.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB;;;;;;;GAOG;AAaH,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,eAAe,CAAC;AAGvB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,KAAK,GAAsC,YAAY,CAAC;AAErE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GACxB,oBAAoB,CAAC;AAEvB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAyC,eAAe,CAAC;AAE9E;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAC3B,uBAAuB,CAAC;AAE1B;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,MAAM,GAAuC,aAAa,CAAC;AAExE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GACzB,qBAAqB,CAAC;AAExB;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,UAAU,GAAsB,iBAAiB,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../../src/component/_generated/server.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB;;;;;;;GAOG;AAaH,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,eAAe,CAAC;AAavB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,KAAK,GAAsC,YAAY,CAAC;AAErE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GACxB,oBAAoB,CAAC;AAEvB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAyC,eAAe,CAAC;AAE9E;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAC3B,uBAAuB,CAAC;AAE1B;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,MAAM,GAAuC,aAAa,CAAC;AAExE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GACzB,qBAAqB,CAAC;AAExB;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,UAAU,GAAsB,iBAAiB,CAAC;AAC/D,MAAM,CAAC,MAAM,GAAG,GAAQ,OAAO,CAAC,GAAqB,CAAC"}
@@ -1,3 +1,20 @@
1
- declare const _default: import("convex/server").ComponentDefinition<any>;
1
+ /**
2
+ * The component declares the env vars it needs so the installing app can wire them in
3
+ * `convex/convex.config.ts` (typically via `app.env.*` so existing project-level env vars
4
+ * pass straight through). All three are read via `process.env` inside the component's
5
+ * actions and cron — `POSTHOG_PERSONAL_API_KEY`'s presence is also what gates the local
6
+ * evaluation refresh cron.
7
+ */
8
+ declare const _default: import("convex/server").ComponentDefinition<any, {
9
+ readonly POSTHOG_PROJECT_TOKEN: import("convex/values").VString<string, "required">;
10
+ readonly POSTHOG_HOST: import("convex/values").VString<string | undefined, "optional">;
11
+ readonly POSTHOG_PERSONAL_API_KEY: import("convex/values").VString<string | undefined, "optional">;
12
+ /**
13
+ * Polling interval for the local-evaluation refresh cron, in whole seconds. Optional
14
+ * (defaults to 60). Convex component env vars are string-typed on the wire, so this is
15
+ * parsed at module load — invalid values log a warning and fall back to the default.
16
+ */
17
+ readonly POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS: import("convex/values").VString<string | undefined, "optional">;
18
+ }>;
2
19
  export default _default;
3
20
  //# sourceMappingURL=convex.config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"convex.config.d.ts","sourceRoot":"","sources":["../../src/component/convex.config.ts"],"names":[],"mappings":";AAEA,wBAAyC"}
1
+ {"version":3,"file":"convex.config.d.ts","sourceRoot":"","sources":["../../src/component/convex.config.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;;;;;IAMC;;;;OAIG;;;AATP,wBAYE"}
@@ -1,3 +1,23 @@
1
1
  import { defineComponent } from 'convex/server';
2
- export default defineComponent('posthog');
2
+ import { v } from 'convex/values';
3
+ /**
4
+ * The component declares the env vars it needs so the installing app can wire them in
5
+ * `convex/convex.config.ts` (typically via `app.env.*` so existing project-level env vars
6
+ * pass straight through). All three are read via `process.env` inside the component's
7
+ * actions and cron — `POSTHOG_PERSONAL_API_KEY`'s presence is also what gates the local
8
+ * evaluation refresh cron.
9
+ */
10
+ export default defineComponent('posthog', {
11
+ env: {
12
+ POSTHOG_PROJECT_TOKEN: v.string(),
13
+ POSTHOG_HOST: v.optional(v.string()),
14
+ POSTHOG_PERSONAL_API_KEY: v.optional(v.string()),
15
+ /**
16
+ * Polling interval for the local-evaluation refresh cron, in whole seconds. Optional
17
+ * (defaults to 60). Convex component env vars are string-typed on the wire, so this is
18
+ * parsed at module load — invalid values log a warning and fall back to the default.
19
+ */
20
+ POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS: v.optional(v.string()),
21
+ },
22
+ });
3
23
  //# sourceMappingURL=convex.config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"convex.config.js","sourceRoot":"","sources":["../../src/component/convex.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAE/C,eAAe,eAAe,CAAC,SAAS,CAAC,CAAA"}
1
+ {"version":3,"file":"convex.config.js","sourceRoot":"","sources":["../../src/component/convex.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC/C,OAAO,EAAE,CAAC,EAAE,MAAM,eAAe,CAAA;AAEjC;;;;;;GAMG;AACH,eAAe,eAAe,CAAC,SAAS,EAAE;IACxC,GAAG,EAAE;QACH,qBAAqB,EAAE,CAAC,CAAC,MAAM,EAAE;QACjC,YAAY,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QACpC,wBAAwB,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAChD;;;;WAIG;QACH,sCAAsC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;KAC/D;CACF,CAAC,CAAA"}
@@ -0,0 +1,12 @@
1
+ declare const crons: import("convex/server").Crons;
2
+ export declare const DEFAULT_INTERVAL_SECONDS = 60;
3
+ /**
4
+ * Parse the optional `POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS` env var into a positive integer.
5
+ *
6
+ * Convex component env vars are string-typed, so we coerce here. Invalid values fall back to
7
+ * the default rather than failing the deploy — flags will still refresh on the default cadence
8
+ * and the operator gets a warning to act on. Exported for unit testing.
9
+ */
10
+ export declare function readPollingIntervalSeconds(): number;
11
+ export default crons;
12
+ //# sourceMappingURL=crons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crons.d.ts","sourceRoot":"","sources":["../../src/component/crons.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,KAAK,+BAAa,CAAA;AAExB,eAAO,MAAM,wBAAwB,KAAK,CAAA;AAE1C;;;;;;GAMG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAYnD;AAwBD,eAAe,KAAK,CAAA"}
@@ -0,0 +1,42 @@
1
+ import { cronJobs } from 'convex/server';
2
+ import { api } from './_generated/api.js';
3
+ import { env } from './_generated/server.js';
4
+ const crons = cronJobs();
5
+ export const DEFAULT_INTERVAL_SECONDS = 60;
6
+ /**
7
+ * Parse the optional `POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS` env var into a positive integer.
8
+ *
9
+ * Convex component env vars are string-typed, so we coerce here. Invalid values fall back to
10
+ * the default rather than failing the deploy — flags will still refresh on the default cadence
11
+ * and the operator gets a warning to act on. Exported for unit testing.
12
+ */
13
+ export function readPollingIntervalSeconds() {
14
+ const raw = (env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS ?? '').trim();
15
+ if (!raw)
16
+ return DEFAULT_INTERVAL_SECONDS;
17
+ const parsed = Number(raw);
18
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
19
+ console.warn(`[PostHog] POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS="${raw}" is not a positive integer; ` +
20
+ `falling back to ${DEFAULT_INTERVAL_SECONDS}s.`);
21
+ return DEFAULT_INTERVAL_SECONDS;
22
+ }
23
+ return parsed;
24
+ }
25
+ /**
26
+ * The refresh cron is registered only when `POSTHOG_PERSONAL_API_KEY` is configured for the
27
+ * component. Without it, local evaluation can't run, so there's no reason to pay the per-tick
28
+ * resource cost — particularly on idle dev deployments on the free tier.
29
+ *
30
+ * Toggling local evaluation on or off therefore requires redeploying the component, which
31
+ * `npx convex env set` triggers automatically in `npx convex dev`. The cron handler itself also
32
+ * guards against a stale registration where the env var was cleared after deploy.
33
+ */
34
+ // Trim before checking, matching how `readConfig()` in `lib.ts` interprets the env var.
35
+ // `npx convex env set` can leave trailing whitespace; without the trim, a value like `" "` would
36
+ // register the cron but then no-op every tick once `readConfig()` rejects the trimmed-to-empty
37
+ // PAK — wasted function calls, especially painful on free-tier deployments.
38
+ if ((env.POSTHOG_PERSONAL_API_KEY ?? '').trim()) {
39
+ crons.interval('Refresh PostHog feature flag definitions', { seconds: readPollingIntervalSeconds() }, api.lib.refreshFlagDefinitions, {});
40
+ }
41
+ export default crons;
42
+ //# sourceMappingURL=crons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crons.js","sourceRoot":"","sources":["../../src/component/crons.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAA;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,wBAAwB,CAAA;AAE5C,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAA;AAExB,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,CAAA;AAE1C;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B;IACxC,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,sCAAsC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IACrE,IAAI,CAAC,GAAG;QAAE,OAAO,wBAAwB,CAAA;IACzC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAA;IAC1B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;QACzE,OAAO,CAAC,IAAI,CACV,qDAAqD,GAAG,+BAA+B;YACrF,mBAAmB,wBAAwB,IAAI,CAClD,CAAA;QACD,OAAO,wBAAwB,CAAA;IACjC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;;;GAQG;AACH,wFAAwF;AACxF,iGAAiG;AACjG,+FAA+F;AAC/F,4EAA4E;AAC5E,IAAI,CAAC,GAAG,CAAC,wBAAwB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;IAChD,KAAK,CAAC,QAAQ,CACZ,0CAA0C,EAC1C,EAAE,OAAO,EAAE,0BAA0B,EAAE,EAAE,EACzC,GAAG,CAAC,GAAG,CAAC,sBAAsB,EAC9B,EAAE,CACH,CAAA;AACH,CAAC;AAED,eAAe,KAAK,CAAA"}
@@ -5,41 +5,31 @@ export declare const capture: import("convex/server").RegisteredAction<"public",
5
5
  sendFeatureFlags?: boolean | undefined;
6
6
  timestamp?: number | undefined;
7
7
  uuid?: string | undefined;
8
- apiKey: string;
9
8
  distinctId: string;
10
- host: string;
11
9
  event: string;
12
10
  }, Promise<void>>;
13
11
  export declare const identify: import("convex/server").RegisteredAction<"public", {
14
12
  disableGeoip?: boolean | undefined;
15
13
  properties?: string | undefined;
16
- apiKey: string;
17
14
  distinctId: string;
18
- host: string;
19
15
  }, Promise<void>>;
20
16
  export declare const groupIdentify: import("convex/server").RegisteredAction<"public", {
21
17
  disableGeoip?: boolean | undefined;
22
18
  distinctId?: string | undefined;
23
19
  properties?: string | undefined;
24
- apiKey: string;
25
- host: string;
26
20
  groupKey: string;
27
21
  groupType: string;
28
22
  }, Promise<void>>;
29
23
  export declare const alias: import("convex/server").RegisteredAction<"public", {
30
24
  disableGeoip?: boolean | undefined;
31
25
  alias: string;
32
- apiKey: string;
33
26
  distinctId: string;
34
- host: string;
35
27
  }, Promise<void>>;
36
28
  export declare const captureException: import("convex/server").RegisteredAction<"public", {
37
29
  distinctId?: string | undefined;
38
30
  additionalProperties?: string | undefined;
39
31
  errorName?: string | undefined;
40
32
  errorStack?: string | undefined;
41
- apiKey: string;
42
- host: string;
43
33
  errorMessage: string;
44
34
  }, Promise<void>>;
45
35
  export declare const evaluateFlag: import("convex/server").RegisteredAction<"public", {
@@ -48,9 +38,7 @@ export declare const evaluateFlag: import("convex/server").RegisteredAction<"pub
48
38
  flagKeys?: string[] | undefined;
49
39
  groupProperties?: any;
50
40
  personProperties?: any;
51
- apiKey: string;
52
41
  distinctId: string;
53
- host: string;
54
42
  key: string;
55
43
  }, Promise<import("@posthog/core").FeatureFlagValue | null>>;
56
44
  export declare const evaluateFlagPayload: import("convex/server").RegisteredAction<"public", {
@@ -59,9 +47,7 @@ export declare const evaluateFlagPayload: import("convex/server").RegisteredActi
59
47
  flagKeys?: string[] | undefined;
60
48
  groupProperties?: any;
61
49
  personProperties?: any;
62
- apiKey: string;
63
50
  distinctId: string;
64
- host: string;
65
51
  key: string;
66
52
  }, Promise<string | number | boolean | {
67
53
  [key: string]: import("@posthog/core").JsonType;
@@ -72,23 +58,32 @@ export declare const evaluateAllFlags: import("convex/server").RegisteredAction<
72
58
  flagKeys?: string[] | undefined;
73
59
  groupProperties?: any;
74
60
  personProperties?: any;
75
- apiKey: string;
76
61
  distinctId: string;
77
- host: string;
78
62
  }, Promise<{
79
63
  featureFlags: Record<string, unknown>;
80
64
  featureFlagPayloads: Record<string, unknown>;
81
65
  }>>;
82
66
  /**
83
- * Returns the latest cached flag definitions, or `null` if none have been fetched yet.
67
+ * Returns the cached flag definitions plus whether local evaluation is configured at all.
84
68
  *
85
- * The `data` field is a JSON-stringified `FlagDefinitions` object (see `client/feature-flags/types.ts`).
69
+ * `localEvalConfigured` reflects whether `POSTHOG_PERSONAL_API_KEY` is set on the component
70
+ * the client uses this to distinguish "you haven't set up local eval" (throw, point the user
71
+ * at the remote `evaluateFlag` methods) from "PAK is set but the cron hasn't fetched yet"
72
+ * (return `undefined` gracefully). `data` is null until the first successful refresh.
73
+ *
74
+ * `data` is a JSON-stringified `FlagDefinitions` object (see `client/feature-flags/types.ts`).
86
75
  */
87
76
  export declare const getFlagDefinitions: import("convex/server").RegisteredQuery<"public", {}, Promise<{
77
+ localEvalConfigured: boolean;
78
+ data: null;
79
+ fetchedAt: null;
80
+ etag: undefined;
81
+ } | {
82
+ localEvalConfigured: boolean;
88
83
  data: string;
89
84
  fetchedAt: number;
90
85
  etag: string | undefined;
91
- } | null>>;
86
+ }>>;
92
87
  export declare const _setFlagDefinitions: import("convex/server").RegisteredMutation<"internal", {
93
88
  etag?: string | undefined;
94
89
  data: string;
@@ -96,20 +91,11 @@ export declare const _setFlagDefinitions: import("convex/server").RegisteredMuta
96
91
  export declare const _getCurrentEtag: import("convex/server").RegisteredQuery<"internal", {}, Promise<string | undefined>>;
97
92
  /**
98
93
  * Fetches flag definitions from PostHog's local-evaluation endpoint and stores them in the
99
- * `flagDefinitions` table. Called by the consumer app's own cron they pass in the keys via the
100
- * `PostHog` client class which knows them already.
101
- *
102
- * Args:
103
- * - `apiKey` — the project API key (`phc_…`)
104
- * - `personalApiKey` — a feature flags secure API key (`phs_…`, recommended) or personal API
105
- * key (`phx_…`) with feature-flag read access; local eval is disabled if missing
106
- * - `host` — optional, defaults to `https://us.i.posthog.com`
94
+ * `flagDefinitions` table. Called automatically by the cron registered in `crons.ts` when
95
+ * `POSTHOG_PERSONAL_API_KEY` is set, and also exposed publicly so the client's
96
+ * `reloadFeatureFlags(ctx)` method (parity with `posthog-node`) can trigger an on-demand refresh.
107
97
  */
108
- export declare const refreshFlagDefinitions: import("convex/server").RegisteredAction<"public", {
109
- host?: string | undefined;
110
- apiKey: string;
111
- personalApiKey: string;
112
- }, Promise<{
98
+ export declare const refreshFlagDefinitions: import("convex/server").RegisteredAction<"public", {}, Promise<{
113
99
  status: "skipped";
114
100
  reason: "missing-keys";
115
101
  } | {
@@ -1 +1 @@
1
- {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AAoDA,eAAO,MAAM,OAAO;;;;;;;;;;;iBA0BlB,CAAA;AAEF,eAAO,MAAM,QAAQ;;;;;;iBA+BnB,CAAA;AAEF,eAAO,MAAM,aAAa;;;;;;;;iBA0BxB,CAAA;AAEF,eAAO,MAAM,KAAK;;;;;;iBAgBhB,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;;;iBAiB3B,CAAA;AAqCF,eAAO,MAAM,YAAY;;;;;;;;;;4DAavB,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;+CAW9B,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;GAe3B,CAAA;AAYF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;;UAO7B,CAAA;AAOF,eAAO,MAAM,mBAAmB;;;iBAW9B,CAAA;AAEF,eAAO,MAAM,eAAe,sFAM1B,CAAA;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmKjC,CAAA"}
1
+ {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AAgFA,eAAO,MAAM,OAAO;;;;;;;;;iBA0BlB,CAAA;AAEF,eAAO,MAAM,QAAQ;;;;iBA+BnB,CAAA;AAEF,eAAO,MAAM,aAAa;;;;;;iBA0BxB,CAAA;AAEF,eAAO,MAAM,KAAK;;;;iBAgBhB,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;iBAiB3B,CAAA;AAmCF,eAAO,MAAM,YAAY;;;;;;;;4DAevB,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;;+CAa9B,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;;;;;GAiB3B,CAAA;AASF;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;GAU7B,CAAA;AAOF,eAAO,MAAM,mBAAmB;;;iBAW9B,CAAA;AAEF,eAAO,MAAM,eAAe,sFAM1B,CAAA;AAEF;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8JjC,CAAA"}
@@ -1,5 +1,5 @@
1
1
  import { PostHog as PostHogEdge } from 'posthog-node/edge';
2
- import { action, internalMutation, internalQuery, query } from './_generated/server.js';
2
+ import { action, env, internalMutation, internalQuery, query } from './_generated/server.js';
3
3
  import { api, internal } from './_generated/api.js';
4
4
  import { v } from 'convex/values';
5
5
  import { version } from './version.js';
@@ -16,6 +16,29 @@ class PostHog extends PostHogEdge {
16
16
  return version;
17
17
  }
18
18
  }
19
+ const DEFAULT_HOST = 'https://us.i.posthog.com';
20
+ /**
21
+ * Resolve the credentials and host the component was configured with.
22
+ *
23
+ * Reads the typed `env` from `_generated/server` (declared in `convex.config.ts`). The
24
+ * installing app wires the values via `app.use(posthog, { env: { ... } })`, typically
25
+ * threading them straight through from its own deployment env vars. Trimming guards
26
+ * against accidental whitespace from `npx convex env set`.
27
+ */
28
+ function readConfig() {
29
+ const projectToken = (env.POSTHOG_PROJECT_TOKEN ?? '').trim();
30
+ const host = (env.POSTHOG_HOST ?? '').trim() || DEFAULT_HOST;
31
+ const personalApiKey = (env.POSTHOG_PERSONAL_API_KEY ?? '').trim();
32
+ if (!projectToken) {
33
+ // Convex's typed env-var validation should prevent an empty `POSTHOG_PROJECT_TOKEN` at deploy time,
34
+ // but the gate is enforced at the app's `convex.config.ts`. Log loudly here so anyone hitting
35
+ // an unexpected empty value (e.g. the token was cleared post-deploy on a stale isolate) has a trail
36
+ // to follow rather than silently dropped events.
37
+ console.warn('[PostHog] POSTHOG_PROJECT_TOKEN is not configured; this event will be dropped. ' +
38
+ 'Set it with `npx convex env set POSTHOG_PROJECT_TOKEN phc_…` and redeploy.');
39
+ }
40
+ return { projectToken, host, personalApiKey };
41
+ }
19
42
  /**
20
43
  * Cache PostHog clients across action invocations within the same Convex isolate.
21
44
  *
@@ -23,14 +46,15 @@ class PostHog extends PostHogEdge {
23
46
  * a fresh client per call (and tearing it down with `shutdown()`) is wasted work — the client
24
47
  * carries no per-invocation state once `flush()` has drained its queue.
25
48
  *
26
- * Keyed by `apiKey|host` to support the rare case of multiple credentials sharing one isolate.
49
+ * Keyed by `projectToken|host` so a deployment that rotates its env vars (via `npx convex env set`)
50
+ * picks up the new client without restarting the isolate.
27
51
  */
28
52
  const clientCache = new Map();
29
- function getClient(apiKey, host) {
30
- const key = `${apiKey}|${host}`;
53
+ function getClient(projectToken, host) {
54
+ const key = `${projectToken}|${host}`;
31
55
  let client = clientCache.get(key);
32
56
  if (!client) {
33
- client = new PostHog(apiKey, { host, flushAt: 1, flushInterval: 0 });
57
+ client = new PostHog(projectToken, { host, flushAt: 1, flushInterval: 0 });
34
58
  clientCache.set(key, client);
35
59
  }
36
60
  return client;
@@ -49,8 +73,6 @@ function parseProperties(json) {
49
73
  }
50
74
  export const capture = action({
51
75
  args: {
52
- apiKey: v.string(),
53
- host: v.string(),
54
76
  distinctId: v.string(),
55
77
  event: v.string(),
56
78
  properties: v.optional(v.string()),
@@ -61,7 +83,10 @@ export const capture = action({
61
83
  disableGeoip: v.optional(v.boolean()),
62
84
  },
63
85
  handler: async (_ctx, args) => {
64
- const client = getClient(args.apiKey, args.host);
86
+ const { projectToken, host } = readConfig();
87
+ if (!projectToken)
88
+ return;
89
+ const client = getClient(projectToken, host);
65
90
  await client.captureImmediate({
66
91
  distinctId: args.distinctId,
67
92
  event: args.event,
@@ -76,14 +101,15 @@ export const capture = action({
76
101
  });
77
102
  export const identify = action({
78
103
  args: {
79
- apiKey: v.string(),
80
- host: v.string(),
81
104
  distinctId: v.string(),
82
105
  properties: v.optional(v.string()),
83
106
  disableGeoip: v.optional(v.boolean()),
84
107
  },
85
108
  handler: async (_ctx, args) => {
86
- const client = getClient(args.apiKey, args.host);
109
+ const { projectToken, host } = readConfig();
110
+ if (!projectToken)
111
+ return;
112
+ const client = getClient(projectToken, host);
87
113
  // posthog-node's `identifyImmediate` is missing an `await` on `identifyStatelessImmediate`
88
114
  // (packages/node/src/client.ts:674), so the returned promise resolves before the event hits
89
115
  // the wire. We sidestep that by composing the `$identify` event the same way `identifyImmediate`
@@ -104,8 +130,6 @@ export const identify = action({
104
130
  });
105
131
  export const groupIdentify = action({
106
132
  args: {
107
- apiKey: v.string(),
108
- host: v.string(),
109
133
  groupType: v.string(),
110
134
  groupKey: v.string(),
111
135
  properties: v.optional(v.string()),
@@ -113,7 +137,10 @@ export const groupIdentify = action({
113
137
  disableGeoip: v.optional(v.boolean()),
114
138
  },
115
139
  handler: async (_ctx, args) => {
116
- const client = getClient(args.apiKey, args.host);
140
+ const { projectToken, host } = readConfig();
141
+ if (!projectToken)
142
+ return;
143
+ const client = getClient(projectToken, host);
117
144
  // posthog-node doesn't expose a `groupIdentifyImmediate`, so we send the same `$groupidentify`
118
145
  // event via `captureImmediate` to keep parity with capture/identify/alias/captureException —
119
146
  // resolve when the network call completes, without resorting to shutdown().
@@ -131,14 +158,15 @@ export const groupIdentify = action({
131
158
  });
132
159
  export const alias = action({
133
160
  args: {
134
- apiKey: v.string(),
135
- host: v.string(),
136
161
  distinctId: v.string(),
137
162
  alias: v.string(),
138
163
  disableGeoip: v.optional(v.boolean()),
139
164
  },
140
165
  handler: async (_ctx, args) => {
141
- const client = getClient(args.apiKey, args.host);
166
+ const { projectToken, host } = readConfig();
167
+ if (!projectToken)
168
+ return;
169
+ const client = getClient(projectToken, host);
142
170
  await client.aliasImmediate({
143
171
  distinctId: args.distinctId,
144
172
  alias: args.alias,
@@ -148,8 +176,6 @@ export const alias = action({
148
176
  });
149
177
  export const captureException = action({
150
178
  args: {
151
- apiKey: v.string(),
152
- host: v.string(),
153
179
  distinctId: v.optional(v.string()),
154
180
  errorMessage: v.string(),
155
181
  errorStack: v.optional(v.string()),
@@ -157,7 +183,10 @@ export const captureException = action({
157
183
  additionalProperties: v.optional(v.string()),
158
184
  },
159
185
  handler: async (_ctx, args) => {
160
- const client = getClient(args.apiKey, args.host);
186
+ const { projectToken, host } = readConfig();
187
+ if (!projectToken)
188
+ return;
189
+ const client = getClient(projectToken, host);
161
190
  const error = new Error(args.errorMessage);
162
191
  if (args.errorName)
163
192
  error.name = args.errorName;
@@ -173,8 +202,6 @@ export const captureException = action({
173
202
  // continuity flags, static cohorts, properties you don't have in your server context). They
174
203
  // require an action context — that's the trade for not needing flag definitions cached upfront.
175
204
  const remoteFlagsArgs = {
176
- apiKey: v.string(),
177
- host: v.string(),
178
205
  distinctId: v.string(),
179
206
  groups: v.optional(v.any()),
180
207
  personProperties: v.optional(v.any()),
@@ -195,7 +222,10 @@ function remoteFlagsOptions(args) {
195
222
  export const evaluateFlag = action({
196
223
  args: { ...remoteFlagsArgs, key: v.string() },
197
224
  handler: async (_ctx, args) => {
198
- const client = getClient(args.apiKey, args.host);
225
+ const { projectToken, host } = readConfig();
226
+ if (!projectToken)
227
+ return null;
228
+ const client = getClient(projectToken, host);
199
229
  // Scope the request to just the flag the caller asked about — otherwise PostHog evaluates
200
230
  // every flag in the project on every call. Honour an explicit `flagKeys` override when given.
201
231
  const snapshot = await client.evaluateFlags(args.distinctId, {
@@ -209,7 +239,10 @@ export const evaluateFlag = action({
209
239
  export const evaluateFlagPayload = action({
210
240
  args: { ...remoteFlagsArgs, key: v.string() },
211
241
  handler: async (_ctx, args) => {
212
- const client = getClient(args.apiKey, args.host);
242
+ const { projectToken, host } = readConfig();
243
+ if (!projectToken)
244
+ return null;
245
+ const client = getClient(projectToken, host);
213
246
  const snapshot = await client.evaluateFlags(args.distinctId, {
214
247
  ...remoteFlagsOptions(args),
215
248
  flagKeys: args.flagKeys ?? [args.key],
@@ -221,7 +254,10 @@ export const evaluateFlagPayload = action({
221
254
  export const evaluateAllFlags = action({
222
255
  args: remoteFlagsArgs,
223
256
  handler: async (_ctx, args) => {
224
- const client = getClient(args.apiKey, args.host);
257
+ const { projectToken, host } = readConfig();
258
+ if (!projectToken)
259
+ return { featureFlags: {}, featureFlagPayloads: {} };
260
+ const client = getClient(projectToken, host);
225
261
  const snapshot = await client.evaluateFlags(args.distinctId, remoteFlagsOptions(args));
226
262
  const featureFlags = {};
227
263
  const featureFlagPayloads = {};
@@ -238,25 +274,29 @@ export const evaluateAllFlags = action({
238
274
  });
239
275
  // --- Feature flag local evaluation ---
240
276
  //
241
- // Feature flag definitions are fetched on demand by `refreshFlagDefinitions` and stored in the
242
- // `flagDefinitions` table. Clients read them via `getFlagDefinitions` and evaluate flags locally
243
- // there is no per-call action for flag evaluation.
244
- //
245
- // The action takes credentials as args. The consumer's app schedules the refresh cron and passes
246
- // them in — typically via `posthog.refreshFlagDefinitions(ctx)` on the client class, which
247
- // forwards the keys it was constructed with.
277
+ // Flag definitions are fetched on a cron registered inside this component (only when
278
+ // `POSTHOG_PERSONAL_API_KEY` is set see `crons.ts`) and stored in the `flagDefinitions`
279
+ // table. Clients read them via `getFlagDefinitions` and evaluate flags locally — there is
280
+ // no per-call action for flag evaluation.
248
281
  /**
249
- * Returns the latest cached flag definitions, or `null` if none have been fetched yet.
282
+ * Returns the cached flag definitions plus whether local evaluation is configured at all.
283
+ *
284
+ * `localEvalConfigured` reflects whether `POSTHOG_PERSONAL_API_KEY` is set on the component —
285
+ * the client uses this to distinguish "you haven't set up local eval" (throw, point the user
286
+ * at the remote `evaluateFlag` methods) from "PAK is set but the cron hasn't fetched yet"
287
+ * (return `undefined` gracefully). `data` is null until the first successful refresh.
250
288
  *
251
- * The `data` field is a JSON-stringified `FlagDefinitions` object (see `client/feature-flags/types.ts`).
289
+ * `data` is a JSON-stringified `FlagDefinitions` object (see `client/feature-flags/types.ts`).
252
290
  */
253
291
  export const getFlagDefinitions = query({
254
292
  args: {},
255
293
  handler: async (ctx) => {
294
+ const localEvalConfigured = !!(env.POSTHOG_PERSONAL_API_KEY ?? '').trim();
256
295
  const row = await ctx.db.query('flagDefinitions').order('desc').first();
257
- if (!row)
258
- return null;
259
- return { data: row.data, fetchedAt: row.fetchedAt, etag: row.etag };
296
+ if (!row) {
297
+ return { localEvalConfigured, data: null, fetchedAt: null, etag: undefined };
298
+ }
299
+ return { localEvalConfigured, data: row.data, fetchedAt: row.fetchedAt, etag: row.etag };
260
300
  },
261
301
  });
262
302
  // All three queries against `flagDefinitions` use `.order('desc').first()` so they all see the
@@ -285,32 +325,22 @@ export const _getCurrentEtag = internalQuery({
285
325
  });
286
326
  /**
287
327
  * Fetches flag definitions from PostHog's local-evaluation endpoint and stores them in the
288
- * `flagDefinitions` table. Called by the consumer app's own cron they pass in the keys via the
289
- * `PostHog` client class which knows them already.
290
- *
291
- * Args:
292
- * - `apiKey` — the project API key (`phc_…`)
293
- * - `personalApiKey` — a feature flags secure API key (`phs_…`, recommended) or personal API
294
- * key (`phx_…`) with feature-flag read access; local eval is disabled if missing
295
- * - `host` — optional, defaults to `https://us.i.posthog.com`
328
+ * `flagDefinitions` table. Called automatically by the cron registered in `crons.ts` when
329
+ * `POSTHOG_PERSONAL_API_KEY` is set, and also exposed publicly so the client's
330
+ * `reloadFeatureFlags(ctx)` method (parity with `posthog-node`) can trigger an on-demand refresh.
296
331
  */
297
332
  export const refreshFlagDefinitions = action({
298
- args: {
299
- apiKey: v.string(),
300
- personalApiKey: v.string(),
301
- host: v.optional(v.string()),
302
- },
303
- handler: async (ctx, args) => {
304
- const projectApiKey = args.apiKey.trim();
305
- const personalApiKey = args.personalApiKey.trim();
306
- const host = (args.host?.trim() || '').replace(/\/$/, '') || 'https://us.i.posthog.com';
307
- if (!projectApiKey || !personalApiKey) {
308
- // Local evaluation requires both keys. Return a status rather than throwing so the caller
309
- // (typically a cron) can surface it cleanly.
333
+ args: {},
334
+ handler: async (ctx) => {
335
+ const { projectToken, host, personalApiKey } = readConfig();
336
+ if (!projectToken || !personalApiKey) {
337
+ // The cron is conditionally registered on `POSTHOG_PERSONAL_API_KEY`, so reaching this branch
338
+ // means either env vars were cleared after deploy (cron still scheduled) or the project token wasn't
339
+ // configured. Return a status rather than throwing so the cron doesn't churn on errors.
310
340
  return { status: 'skipped', reason: 'missing-keys' };
311
341
  }
312
342
  const etag = await ctx.runQuery(internal.lib._getCurrentEtag, {});
313
- const url = `${host}/flags/definitions?token=${projectApiKey}&send_cohorts`;
343
+ const url = `${host.replace(/\/$/, '')}/flags/definitions?token=${projectToken}&send_cohorts`;
314
344
  const headers = {
315
345
  'Content-Type': 'application/json',
316
346
  Authorization: `Bearer ${personalApiKey}`,
@@ -390,7 +420,7 @@ export const refreshFlagDefinitions = action({
390
420
  if (looksCacheCold) {
391
421
  const existing = await ctx.runQuery(api.lib.getFlagDefinitions, {});
392
422
  const STALE_AFTER_MS = 5 * 60 * 1000;
393
- if (existing === null) {
423
+ if (existing.fetchedAt === null) {
394
424
  // No prior cache — write an empty snapshot so subsequent reads are deterministic and
395
425
  // the UI shows "no flags" instead of "loading".
396
426
  await ctx.runMutation(internal.lib._setFlagDefinitions, {
@@ -414,7 +444,7 @@ export const refreshFlagDefinitions = action({
414
444
  // Recent cached defs — keep them while PostHog's cache potentially warms back up.
415
445
  return { status: 'stale' };
416
446
  }
417
- console.warn(`[PostHog] Unexpected status ${response.status} fetching flag definitions from ${url.replace(projectApiKey, '<token>')}. ` +
447
+ console.warn(`[PostHog] Unexpected status ${response.status} fetching flag definitions from ${url.replace(projectToken, '<token>')}. ` +
418
448
  `Response body: ${bodyText}`);
419
449
  return { status: 'error', reason: 'unexpected-status' };
420
450
  }