@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.
- 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 +12 -0
- package/dist/component/crons.d.ts.map +1 -0
- package/dist/component/crons.js +42 -0
- package/dist/component/crons.js.map +1 -0
- package/dist/component/lib.d.ts +18 -32
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +90 -60
- package/dist/component/lib.js.map +1 -1
- package/dist/component/version.d.ts +1 -1
- package/dist/component/version.js +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 +62 -0
- package/src/component/crons.ts +52 -0
- package/src/component/lib.ts +86 -59
- package/src/component/version.ts +1 -1
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
|
/**
|
|
@@ -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
|
-
{
|
|
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
|
-
|
|
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
|