@posthog/convex 1.0.9 → 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 (39) 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.js +1 -1
  29. package/package.json +7 -6
  30. package/src/client/index.test.ts +85 -63
  31. package/src/client/index.ts +35 -60
  32. package/src/component/_generated/api.ts +4 -0
  33. package/src/component/_generated/component.ts +3 -27
  34. package/src/component/_generated/server.ts +11 -0
  35. package/src/component/convex.config.ts +21 -1
  36. package/src/component/crons.test.ts +62 -0
  37. package/src/component/crons.ts +52 -0
  38. package/src/component/lib.ts +86 -59
  39. package/src/component/version.ts +1 -1
@@ -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
  /**
@@ -27,13 +27,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
27
27
  alias: FunctionReference<
28
28
  "action",
29
29
  "internal",
30
- {
31
- alias: string;
32
- apiKey: string;
33
- disableGeoip?: boolean;
34
- distinctId: string;
35
- host: string;
36
- },
30
+ { alias: string; disableGeoip?: boolean; distinctId: string },
37
31
  any,
38
32
  Name
39
33
  >;
@@ -41,12 +35,10 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
41
35
  "action",
42
36
  "internal",
43
37
  {
44
- apiKey: string;
45
38
  disableGeoip?: boolean;
46
39
  distinctId: string;
47
40
  event: string;
48
41
  groups?: string;
49
- host: string;
50
42
  properties?: string;
51
43
  sendFeatureFlags?: boolean;
52
44
  timestamp?: number;
@@ -60,12 +52,10 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
60
52
  "internal",
61
53
  {
62
54
  additionalProperties?: string;
63
- apiKey: string;
64
55
  distinctId?: string;
65
56
  errorMessage: string;
66
57
  errorName?: string;
67
58
  errorStack?: string;
68
- host: string;
69
59
  },
70
60
  any,
71
61
  Name
@@ -74,13 +64,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
74
64
  "action",
75
65
  "internal",
76
66
  {
77
- apiKey: string;
78
67
  disableGeoip?: boolean;
79
68
  distinctId: string;
80
69
  flagKeys?: Array<string>;
81
70
  groupProperties?: any;
82
71
  groups?: any;
83
- host: string;
84
72
  personProperties?: any;
85
73
  },
86
74
  any,
@@ -90,13 +78,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
90
78
  "action",
91
79
  "internal",
92
80
  {
93
- apiKey: string;
94
81
  disableGeoip?: boolean;
95
82
  distinctId: string;
96
83
  flagKeys?: Array<string>;
97
84
  groupProperties?: any;
98
85
  groups?: any;
99
- host: string;
100
86
  key: string;
101
87
  personProperties?: any;
102
88
  },
@@ -107,13 +93,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
107
93
  "action",
108
94
  "internal",
109
95
  {
110
- apiKey: string;
111
96
  disableGeoip?: boolean;
112
97
  distinctId: string;
113
98
  flagKeys?: Array<string>;
114
99
  groupProperties?: any;
115
100
  groups?: any;
116
- host: string;
117
101
  key: string;
118
102
  personProperties?: any;
119
103
  },
@@ -125,12 +109,10 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
125
109
  "action",
126
110
  "internal",
127
111
  {
128
- apiKey: string;
129
112
  disableGeoip?: boolean;
130
113
  distinctId?: string;
131
114
  groupKey: string;
132
115
  groupType: string;
133
- host: string;
134
116
  properties?: string;
135
117
  },
136
118
  any,
@@ -139,20 +121,14 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
139
121
  identify: FunctionReference<
140
122
  "action",
141
123
  "internal",
142
- {
143
- apiKey: string;
144
- disableGeoip?: boolean;
145
- distinctId: string;
146
- host: string;
147
- properties?: string;
148
- },
124
+ { disableGeoip?: boolean; distinctId: string; properties?: string },
149
125
  any,
150
126
  Name
151
127
  >;
152
128
  refreshFlagDefinitions: FunctionReference<
153
129
  "action",
154
130
  "internal",
155
- { apiKey: string; host?: string; personalApiKey: string },
131
+ {},
156
132
  any,
157
133
  Name
158
134
  >;
@@ -30,6 +30,16 @@ import {
30
30
  } from "convex/server";
31
31
  import type { DataModel } from "./dataModel.js";
32
32
 
33
+ /**
34
+ * Typesafe environment variables declared in `convex.config.ts`.
35
+ */
36
+ type Env = {
37
+ readonly POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS: string | undefined;
38
+ readonly POSTHOG_HOST: string | undefined;
39
+ readonly POSTHOG_PERSONAL_API_KEY: string | undefined;
40
+ readonly POSTHOG_PROJECT_TOKEN: string;
41
+ };
42
+
33
43
  /**
34
44
  * Define a query in this Convex app's public API.
35
45
  *
@@ -106,6 +116,7 @@ export const internalAction: ActionBuilder<DataModel, "internal"> =
106
116
  * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
107
117
  */
108
118
  export const httpAction: HttpActionBuilder = httpActionGeneric;
119
+ export const env: Env = process.env as unknown as Env;
109
120
 
110
121
  /**
111
122
  * A set of services for use within Convex query functions.
@@ -1,3 +1,23 @@
1
1
  import { defineComponent } from 'convex/server'
2
+ import { v } from 'convex/values'
2
3
 
3
- export default defineComponent('posthog')
4
+ /**
5
+ * The component declares the env vars it needs so the installing app can wire them in
6
+ * `convex/convex.config.ts` (typically via `app.env.*` so existing project-level env vars
7
+ * pass straight through). All three are read via `process.env` inside the component's
8
+ * actions and cron — `POSTHOG_PERSONAL_API_KEY`'s presence is also what gates the local
9
+ * evaluation refresh cron.
10
+ */
11
+ export default defineComponent('posthog', {
12
+ env: {
13
+ POSTHOG_PROJECT_TOKEN: v.string(),
14
+ POSTHOG_HOST: v.optional(v.string()),
15
+ POSTHOG_PERSONAL_API_KEY: v.optional(v.string()),
16
+ /**
17
+ * Polling interval for the local-evaluation refresh cron, in whole seconds. Optional
18
+ * (defaults to 60). Convex component env vars are string-typed on the wire, so this is
19
+ * parsed at module load — invalid values log a warning and fall back to the default.
20
+ */
21
+ POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS: v.optional(v.string()),
22
+ },
23
+ })
@@ -0,0 +1,62 @@
1
+ import { describe, expect, test, beforeEach, afterEach, jest } from '@jest/globals'
2
+ import { DEFAULT_INTERVAL_SECONDS, readPollingIntervalSeconds } from './crons.js'
3
+
4
+ describe('readPollingIntervalSeconds', () => {
5
+ let warnSpy: ReturnType<typeof jest.spyOn>
6
+
7
+ beforeEach(() => {
8
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
9
+ })
10
+
11
+ afterEach(() => {
12
+ warnSpy.mockRestore()
13
+ delete process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS
14
+ })
15
+
16
+ test('returns the default when the env var is unset', () => {
17
+ expect(readPollingIntervalSeconds()).toBe(DEFAULT_INTERVAL_SECONDS)
18
+ expect(warnSpy).not.toHaveBeenCalled()
19
+ })
20
+
21
+ test('returns the default when the env var is empty or whitespace', () => {
22
+ process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = ' '
23
+ expect(readPollingIntervalSeconds()).toBe(DEFAULT_INTERVAL_SECONDS)
24
+ expect(warnSpy).not.toHaveBeenCalled()
25
+ })
26
+
27
+ test('parses a positive integer', () => {
28
+ process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = '300'
29
+ expect(readPollingIntervalSeconds()).toBe(300)
30
+ expect(warnSpy).not.toHaveBeenCalled()
31
+ })
32
+
33
+ test('trims whitespace before parsing', () => {
34
+ process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = ' 120 '
35
+ expect(readPollingIntervalSeconds()).toBe(120)
36
+ expect(warnSpy).not.toHaveBeenCalled()
37
+ })
38
+
39
+ test('warns and falls back for non-numeric input', () => {
40
+ process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = 'abc'
41
+ expect(readPollingIntervalSeconds()).toBe(DEFAULT_INTERVAL_SECONDS)
42
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('"abc"'))
43
+ })
44
+
45
+ test('warns and falls back for zero', () => {
46
+ process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = '0'
47
+ expect(readPollingIntervalSeconds()).toBe(DEFAULT_INTERVAL_SECONDS)
48
+ expect(warnSpy).toHaveBeenCalled()
49
+ })
50
+
51
+ test('warns and falls back for negative values', () => {
52
+ process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = '-30'
53
+ expect(readPollingIntervalSeconds()).toBe(DEFAULT_INTERVAL_SECONDS)
54
+ expect(warnSpy).toHaveBeenCalled()
55
+ })
56
+
57
+ test('warns and falls back for fractional values', () => {
58
+ process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = '60.5'
59
+ expect(readPollingIntervalSeconds()).toBe(DEFAULT_INTERVAL_SECONDS)
60
+ expect(warnSpy).toHaveBeenCalled()
61
+ })
62
+ })
@@ -0,0 +1,52 @@
1
+ import { cronJobs } from 'convex/server'
2
+ import { api } from './_generated/api.js'
3
+ import { env } from './_generated/server.js'
4
+
5
+ const crons = cronJobs()
6
+
7
+ export const DEFAULT_INTERVAL_SECONDS = 60
8
+
9
+ /**
10
+ * Parse the optional `POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS` env var into a positive integer.
11
+ *
12
+ * Convex component env vars are string-typed, so we coerce here. Invalid values fall back to
13
+ * the default rather than failing the deploy — flags will still refresh on the default cadence
14
+ * and the operator gets a warning to act on. Exported for unit testing.
15
+ */
16
+ export function readPollingIntervalSeconds(): number {
17
+ const raw = (env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS ?? '').trim()
18
+ if (!raw) return DEFAULT_INTERVAL_SECONDS
19
+ const parsed = Number(raw)
20
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
21
+ console.warn(
22
+ `[PostHog] POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS="${raw}" is not a positive integer; ` +
23
+ `falling back to ${DEFAULT_INTERVAL_SECONDS}s.`
24
+ )
25
+ return DEFAULT_INTERVAL_SECONDS
26
+ }
27
+ return parsed
28
+ }
29
+
30
+ /**
31
+ * The refresh cron is registered only when `POSTHOG_PERSONAL_API_KEY` is configured for the
32
+ * component. Without it, local evaluation can't run, so there's no reason to pay the per-tick
33
+ * resource cost — particularly on idle dev deployments on the free tier.
34
+ *
35
+ * Toggling local evaluation on or off therefore requires redeploying the component, which
36
+ * `npx convex env set` triggers automatically in `npx convex dev`. The cron handler itself also
37
+ * guards against a stale registration where the env var was cleared after deploy.
38
+ */
39
+ // Trim before checking, matching how `readConfig()` in `lib.ts` interprets the env var.
40
+ // `npx convex env set` can leave trailing whitespace; without the trim, a value like `" "` would
41
+ // register the cron but then no-op every tick once `readConfig()` rejects the trimmed-to-empty
42
+ // PAK — wasted function calls, especially painful on free-tier deployments.
43
+ if ((env.POSTHOG_PERSONAL_API_KEY ?? '').trim()) {
44
+ crons.interval(
45
+ 'Refresh PostHog feature flag definitions',
46
+ { seconds: readPollingIntervalSeconds() },
47
+ api.lib.refreshFlagDefinitions,
48
+ {}
49
+ )
50
+ }
51
+
52
+ export default crons