@posthog/convex 1.0.10 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -36
- package/dist/client/index.d.ts +17 -22
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +26 -41
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +4 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +1 -21
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +11 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js +1 -0
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/convex.config.d.ts +18 -1
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +21 -1
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/crons.d.ts +5 -0
- package/dist/component/crons.d.ts.map +1 -0
- package/dist/component/crons.js +26 -0
- package/dist/component/crons.js.map +1 -0
- package/dist/component/lib.d.ts +11 -35
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +84 -63
- package/dist/component/lib.js.map +1 -1
- package/dist/component/version.d.ts +1 -1
- package/dist/component/version.d.ts.map +1 -1
- package/dist/component/version.js +1 -1
- package/dist/component/version.js.map +1 -1
- package/package.json +7 -6
- package/src/client/index.test.ts +85 -63
- package/src/client/index.ts +35 -60
- package/src/component/_generated/api.ts +4 -0
- package/src/component/_generated/component.ts +3 -27
- package/src/component/_generated/server.ts +11 -0
- package/src/component/convex.config.ts +21 -1
- package/src/component/crons.test.ts +96 -0
- package/src/component/crons.ts +36 -0
- package/src/component/lib.ts +80 -62
- package/src/component/version.ts +1 -1
|
@@ -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,96 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach, jest } from '@jest/globals'
|
|
2
|
+
import type { Crons } from 'convex/server'
|
|
3
|
+
import { DEFAULT_INTERVAL_SECONDS, readPollingIntervalSeconds } from './crons.js'
|
|
4
|
+
|
|
5
|
+
describe('cron registration', () => {
|
|
6
|
+
let originalPak: string | undefined
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
originalPak = process.env.POSTHOG_PERSONAL_API_KEY
|
|
10
|
+
jest.resetModules()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
if (originalPak === undefined) {
|
|
15
|
+
delete process.env.POSTHOG_PERSONAL_API_KEY
|
|
16
|
+
} else {
|
|
17
|
+
process.env.POSTHOG_PERSONAL_API_KEY = originalPak
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Convex forwards component env vars only at runtime, so deploy-time module analysis sees
|
|
22
|
+
// `process.env.POSTHOG_PERSONAL_API_KEY` empty even when the installing app has set it.
|
|
23
|
+
// A load-time gate would silently drop the cron — see #3683.
|
|
24
|
+
test.each<[string, string | undefined]>([
|
|
25
|
+
['unset', undefined],
|
|
26
|
+
['set', 'phx_test'],
|
|
27
|
+
])('registers the refresh cron when POSTHOG_PERSONAL_API_KEY is %s at module load', async (_label, pakValue) => {
|
|
28
|
+
if (pakValue === undefined) {
|
|
29
|
+
delete process.env.POSTHOG_PERSONAL_API_KEY
|
|
30
|
+
} else {
|
|
31
|
+
process.env.POSTHOG_PERSONAL_API_KEY = pakValue
|
|
32
|
+
}
|
|
33
|
+
const mod = (await import('./crons.js')) as { default: Crons }
|
|
34
|
+
expect(Object.keys(mod.default.crons)).toContain('Refresh PostHog feature flag definitions')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('readPollingIntervalSeconds', () => {
|
|
39
|
+
let warnSpy: ReturnType<typeof jest.spyOn>
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
warnSpy.mockRestore()
|
|
47
|
+
delete process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('returns the default when the env var is unset', () => {
|
|
51
|
+
expect(readPollingIntervalSeconds()).toBe(DEFAULT_INTERVAL_SECONDS)
|
|
52
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('returns the default when the env var is empty or whitespace', () => {
|
|
56
|
+
process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = ' '
|
|
57
|
+
expect(readPollingIntervalSeconds()).toBe(DEFAULT_INTERVAL_SECONDS)
|
|
58
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('parses a positive integer', () => {
|
|
62
|
+
process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = '300'
|
|
63
|
+
expect(readPollingIntervalSeconds()).toBe(300)
|
|
64
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('trims whitespace before parsing', () => {
|
|
68
|
+
process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = ' 120 '
|
|
69
|
+
expect(readPollingIntervalSeconds()).toBe(120)
|
|
70
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('warns and falls back for non-numeric input', () => {
|
|
74
|
+
process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = 'abc'
|
|
75
|
+
expect(readPollingIntervalSeconds()).toBe(DEFAULT_INTERVAL_SECONDS)
|
|
76
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('"abc"'))
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('warns and falls back for zero', () => {
|
|
80
|
+
process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = '0'
|
|
81
|
+
expect(readPollingIntervalSeconds()).toBe(DEFAULT_INTERVAL_SECONDS)
|
|
82
|
+
expect(warnSpy).toHaveBeenCalled()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('warns and falls back for negative values', () => {
|
|
86
|
+
process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = '-30'
|
|
87
|
+
expect(readPollingIntervalSeconds()).toBe(DEFAULT_INTERVAL_SECONDS)
|
|
88
|
+
expect(warnSpy).toHaveBeenCalled()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('warns and falls back for fractional values', () => {
|
|
92
|
+
process.env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS = '60.5'
|
|
93
|
+
expect(readPollingIntervalSeconds()).toBe(DEFAULT_INTERVAL_SECONDS)
|
|
94
|
+
expect(warnSpy).toHaveBeenCalled()
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
// Override via `POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS`.
|
|
8
|
+
export const DEFAULT_INTERVAL_SECONDS = 60
|
|
9
|
+
|
|
10
|
+
// Convex component env vars are string-typed. Invalid values warn and fall back rather than
|
|
11
|
+
// failing the deploy. Exported for unit testing.
|
|
12
|
+
export function readPollingIntervalSeconds(): number {
|
|
13
|
+
const raw = (env.POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS ?? '').trim()
|
|
14
|
+
if (!raw) return DEFAULT_INTERVAL_SECONDS
|
|
15
|
+
const parsed = Number(raw)
|
|
16
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
|
|
17
|
+
console.warn(
|
|
18
|
+
`[PostHog] POSTHOG_FLAGS_POLLING_INTERVAL_SECONDS="${raw}" is not a positive integer; ` +
|
|
19
|
+
`falling back to ${DEFAULT_INTERVAL_SECONDS}s.`
|
|
20
|
+
)
|
|
21
|
+
return DEFAULT_INTERVAL_SECONDS
|
|
22
|
+
}
|
|
23
|
+
return parsed
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Registered unconditionally — Convex forwards component env vars only at runtime, so a
|
|
27
|
+
// load-time gate on `POSTHOG_PERSONAL_API_KEY` sees an empty value at deploy-time module
|
|
28
|
+
// analysis and silently drops the cron. The handler in `lib.ts` gates at runtime instead.
|
|
29
|
+
crons.interval(
|
|
30
|
+
'Refresh PostHog feature flag definitions',
|
|
31
|
+
{ seconds: readPollingIntervalSeconds() },
|
|
32
|
+
api.lib.refreshFlagDefinitions,
|
|
33
|
+
{}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
export default crons
|
package/src/component/lib.ts
CHANGED
|
@@ -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'
|
|
@@ -18,6 +18,33 @@ class PostHog extends PostHogEdge {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const DEFAULT_HOST = 'https://us.i.posthog.com'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the credentials and host the component was configured with.
|
|
25
|
+
*
|
|
26
|
+
* Reads the typed `env` from `_generated/server` (declared in `convex.config.ts`). The
|
|
27
|
+
* installing app wires the values via `app.use(posthog, { env: { ... } })`, typically
|
|
28
|
+
* threading them straight through from its own deployment env vars. Trimming guards
|
|
29
|
+
* against accidental whitespace from `npx convex env set`.
|
|
30
|
+
*/
|
|
31
|
+
function readConfig(): { projectToken: string; host: string; personalApiKey: string } {
|
|
32
|
+
const projectToken = (env.POSTHOG_PROJECT_TOKEN ?? '').trim()
|
|
33
|
+
const host = (env.POSTHOG_HOST ?? '').trim() || DEFAULT_HOST
|
|
34
|
+
const personalApiKey = (env.POSTHOG_PERSONAL_API_KEY ?? '').trim()
|
|
35
|
+
if (!projectToken) {
|
|
36
|
+
// Convex's typed env-var validation should prevent an empty `POSTHOG_PROJECT_TOKEN` at deploy time,
|
|
37
|
+
// but the gate is enforced at the app's `convex.config.ts`. Log loudly here so anyone hitting
|
|
38
|
+
// an unexpected empty value (e.g. the token was cleared post-deploy on a stale isolate) has a trail
|
|
39
|
+
// to follow rather than silently dropped events.
|
|
40
|
+
console.warn(
|
|
41
|
+
'[PostHog] POSTHOG_PROJECT_TOKEN is not configured; this event will be dropped. ' +
|
|
42
|
+
'Set it with `npx convex env set POSTHOG_PROJECT_TOKEN phc_…` and redeploy.'
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
return { projectToken, host, personalApiKey }
|
|
46
|
+
}
|
|
47
|
+
|
|
21
48
|
/**
|
|
22
49
|
* Cache PostHog clients across action invocations within the same Convex isolate.
|
|
23
50
|
*
|
|
@@ -25,15 +52,16 @@ class PostHog extends PostHogEdge {
|
|
|
25
52
|
* a fresh client per call (and tearing it down with `shutdown()`) is wasted work — the client
|
|
26
53
|
* carries no per-invocation state once `flush()` has drained its queue.
|
|
27
54
|
*
|
|
28
|
-
* Keyed by `
|
|
55
|
+
* Keyed by `projectToken|host` so a deployment that rotates its env vars (via `npx convex env set`)
|
|
56
|
+
* picks up the new client without restarting the isolate.
|
|
29
57
|
*/
|
|
30
58
|
const clientCache = new Map<string, PostHog>()
|
|
31
59
|
|
|
32
|
-
function getClient(
|
|
33
|
-
const key = `${
|
|
60
|
+
function getClient(projectToken: string, host: string): PostHog {
|
|
61
|
+
const key = `${projectToken}|${host}`
|
|
34
62
|
let client = clientCache.get(key)
|
|
35
63
|
if (!client) {
|
|
36
|
-
client = new PostHog(
|
|
64
|
+
client = new PostHog(projectToken, { host, flushAt: 1, flushInterval: 0 })
|
|
37
65
|
clientCache.set(key, client)
|
|
38
66
|
}
|
|
39
67
|
return client
|
|
@@ -52,8 +80,6 @@ function parseProperties(json: string | undefined): Record<string, unknown> | un
|
|
|
52
80
|
|
|
53
81
|
export const capture = action({
|
|
54
82
|
args: {
|
|
55
|
-
apiKey: v.string(),
|
|
56
|
-
host: v.string(),
|
|
57
83
|
distinctId: v.string(),
|
|
58
84
|
event: v.string(),
|
|
59
85
|
properties: v.optional(v.string()),
|
|
@@ -64,7 +90,9 @@ export const capture = action({
|
|
|
64
90
|
disableGeoip: v.optional(v.boolean()),
|
|
65
91
|
},
|
|
66
92
|
handler: async (_ctx, args) => {
|
|
67
|
-
const
|
|
93
|
+
const { projectToken, host } = readConfig()
|
|
94
|
+
if (!projectToken) return
|
|
95
|
+
const client = getClient(projectToken, host)
|
|
68
96
|
await client.captureImmediate({
|
|
69
97
|
distinctId: args.distinctId,
|
|
70
98
|
event: args.event,
|
|
@@ -80,14 +108,14 @@ export const capture = action({
|
|
|
80
108
|
|
|
81
109
|
export const identify = action({
|
|
82
110
|
args: {
|
|
83
|
-
apiKey: v.string(),
|
|
84
|
-
host: v.string(),
|
|
85
111
|
distinctId: v.string(),
|
|
86
112
|
properties: v.optional(v.string()),
|
|
87
113
|
disableGeoip: v.optional(v.boolean()),
|
|
88
114
|
},
|
|
89
115
|
handler: async (_ctx, args) => {
|
|
90
|
-
const
|
|
116
|
+
const { projectToken, host } = readConfig()
|
|
117
|
+
if (!projectToken) return
|
|
118
|
+
const client = getClient(projectToken, host)
|
|
91
119
|
// posthog-node's `identifyImmediate` is missing an `await` on `identifyStatelessImmediate`
|
|
92
120
|
// (packages/node/src/client.ts:674), so the returned promise resolves before the event hits
|
|
93
121
|
// the wire. We sidestep that by composing the `$identify` event the same way `identifyImmediate`
|
|
@@ -113,8 +141,6 @@ export const identify = action({
|
|
|
113
141
|
|
|
114
142
|
export const groupIdentify = action({
|
|
115
143
|
args: {
|
|
116
|
-
apiKey: v.string(),
|
|
117
|
-
host: v.string(),
|
|
118
144
|
groupType: v.string(),
|
|
119
145
|
groupKey: v.string(),
|
|
120
146
|
properties: v.optional(v.string()),
|
|
@@ -122,7 +148,9 @@ export const groupIdentify = action({
|
|
|
122
148
|
disableGeoip: v.optional(v.boolean()),
|
|
123
149
|
},
|
|
124
150
|
handler: async (_ctx, args) => {
|
|
125
|
-
const
|
|
151
|
+
const { projectToken, host } = readConfig()
|
|
152
|
+
if (!projectToken) return
|
|
153
|
+
const client = getClient(projectToken, host)
|
|
126
154
|
// posthog-node doesn't expose a `groupIdentifyImmediate`, so we send the same `$groupidentify`
|
|
127
155
|
// event via `captureImmediate` to keep parity with capture/identify/alias/captureException —
|
|
128
156
|
// resolve when the network call completes, without resorting to shutdown().
|
|
@@ -141,14 +169,14 @@ export const groupIdentify = action({
|
|
|
141
169
|
|
|
142
170
|
export const alias = action({
|
|
143
171
|
args: {
|
|
144
|
-
apiKey: v.string(),
|
|
145
|
-
host: v.string(),
|
|
146
172
|
distinctId: v.string(),
|
|
147
173
|
alias: v.string(),
|
|
148
174
|
disableGeoip: v.optional(v.boolean()),
|
|
149
175
|
},
|
|
150
176
|
handler: async (_ctx, args) => {
|
|
151
|
-
const
|
|
177
|
+
const { projectToken, host } = readConfig()
|
|
178
|
+
if (!projectToken) return
|
|
179
|
+
const client = getClient(projectToken, host)
|
|
152
180
|
await client.aliasImmediate({
|
|
153
181
|
distinctId: args.distinctId,
|
|
154
182
|
alias: args.alias,
|
|
@@ -159,8 +187,6 @@ export const alias = action({
|
|
|
159
187
|
|
|
160
188
|
export const captureException = action({
|
|
161
189
|
args: {
|
|
162
|
-
apiKey: v.string(),
|
|
163
|
-
host: v.string(),
|
|
164
190
|
distinctId: v.optional(v.string()),
|
|
165
191
|
errorMessage: v.string(),
|
|
166
192
|
errorStack: v.optional(v.string()),
|
|
@@ -168,7 +194,9 @@ export const captureException = action({
|
|
|
168
194
|
additionalProperties: v.optional(v.string()),
|
|
169
195
|
},
|
|
170
196
|
handler: async (_ctx, args) => {
|
|
171
|
-
const
|
|
197
|
+
const { projectToken, host } = readConfig()
|
|
198
|
+
if (!projectToken) return
|
|
199
|
+
const client = getClient(projectToken, host)
|
|
172
200
|
const error = new Error(args.errorMessage)
|
|
173
201
|
if (args.errorName) error.name = args.errorName
|
|
174
202
|
if (args.errorStack) error.stack = args.errorStack
|
|
@@ -184,8 +212,6 @@ export const captureException = action({
|
|
|
184
212
|
// require an action context — that's the trade for not needing flag definitions cached upfront.
|
|
185
213
|
|
|
186
214
|
const remoteFlagsArgs = {
|
|
187
|
-
apiKey: v.string(),
|
|
188
|
-
host: v.string(),
|
|
189
215
|
distinctId: v.string(),
|
|
190
216
|
groups: v.optional(v.any()),
|
|
191
217
|
personProperties: v.optional(v.any()),
|
|
@@ -214,7 +240,9 @@ function remoteFlagsOptions(args: {
|
|
|
214
240
|
export const evaluateFlag = action({
|
|
215
241
|
args: { ...remoteFlagsArgs, key: v.string() },
|
|
216
242
|
handler: async (_ctx, args) => {
|
|
217
|
-
const
|
|
243
|
+
const { projectToken, host } = readConfig()
|
|
244
|
+
if (!projectToken) return null
|
|
245
|
+
const client = getClient(projectToken, host)
|
|
218
246
|
// Scope the request to just the flag the caller asked about — otherwise PostHog evaluates
|
|
219
247
|
// every flag in the project on every call. Honour an explicit `flagKeys` override when given.
|
|
220
248
|
const snapshot = await client.evaluateFlags(args.distinctId, {
|
|
@@ -229,7 +257,9 @@ export const evaluateFlag = action({
|
|
|
229
257
|
export const evaluateFlagPayload = action({
|
|
230
258
|
args: { ...remoteFlagsArgs, key: v.string() },
|
|
231
259
|
handler: async (_ctx, args) => {
|
|
232
|
-
const
|
|
260
|
+
const { projectToken, host } = readConfig()
|
|
261
|
+
if (!projectToken) return null
|
|
262
|
+
const client = getClient(projectToken, host)
|
|
233
263
|
const snapshot = await client.evaluateFlags(args.distinctId, {
|
|
234
264
|
...remoteFlagsOptions(args),
|
|
235
265
|
flagKeys: args.flagKeys ?? [args.key],
|
|
@@ -242,7 +272,9 @@ export const evaluateFlagPayload = action({
|
|
|
242
272
|
export const evaluateAllFlags = action({
|
|
243
273
|
args: remoteFlagsArgs,
|
|
244
274
|
handler: async (_ctx, args) => {
|
|
245
|
-
const
|
|
275
|
+
const { projectToken, host } = readConfig()
|
|
276
|
+
if (!projectToken) return { featureFlags: {}, featureFlagPayloads: {} }
|
|
277
|
+
const client = getClient(projectToken, host)
|
|
246
278
|
const snapshot = await client.evaluateFlags(args.distinctId, remoteFlagsOptions(args))
|
|
247
279
|
const featureFlags: Record<string, unknown> = {}
|
|
248
280
|
const featureFlagPayloads: Record<string, unknown> = {}
|
|
@@ -258,25 +290,23 @@ export const evaluateAllFlags = action({
|
|
|
258
290
|
|
|
259
291
|
// --- Feature flag local evaluation ---
|
|
260
292
|
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
// The action takes credentials as args. The consumer's app schedules the refresh cron and passes
|
|
266
|
-
// them in — typically via `posthog.refreshFlagDefinitions(ctx)` on the client class, which
|
|
267
|
-
// forwards the keys it was constructed with.
|
|
293
|
+
// Flag definitions are fetched on the cron in `crons.ts` and stored in `flagDefinitions`.
|
|
294
|
+
// Clients read them via `getFlagDefinitions` and evaluate flags locally — there is no
|
|
295
|
+
// per-call action for flag evaluation.
|
|
268
296
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
*/
|
|
297
|
+
// `localEvalConfigured` lets the client distinguish "PAK not set" (throw, point at the
|
|
298
|
+
// remote `evaluateFlag` methods) from "PAK set but cron hasn't fetched yet" (return
|
|
299
|
+
// `undefined`). `data` is a JSON-stringified `FlagDefinitions` (see
|
|
300
|
+
// `client/feature-flags/types.ts`), null until the first successful refresh.
|
|
274
301
|
export const getFlagDefinitions = query({
|
|
275
302
|
args: {},
|
|
276
303
|
handler: async (ctx) => {
|
|
304
|
+
const localEvalConfigured = !!(env.POSTHOG_PERSONAL_API_KEY ?? '').trim()
|
|
277
305
|
const row = await ctx.db.query('flagDefinitions').order('desc').first()
|
|
278
|
-
if (!row)
|
|
279
|
-
|
|
306
|
+
if (!row) {
|
|
307
|
+
return { localEvalConfigured, data: null, fetchedAt: null, etag: undefined }
|
|
308
|
+
}
|
|
309
|
+
return { localEvalConfigured, data: row.data, fetchedAt: row.fetchedAt, etag: row.etag }
|
|
280
310
|
},
|
|
281
311
|
})
|
|
282
312
|
|
|
@@ -308,35 +338,23 @@ export const _getCurrentEtag = internalQuery({
|
|
|
308
338
|
|
|
309
339
|
/**
|
|
310
340
|
* Fetches flag definitions from PostHog's local-evaluation endpoint and stores them in the
|
|
311
|
-
* `flagDefinitions` table. Called by the
|
|
312
|
-
* `
|
|
313
|
-
*
|
|
314
|
-
* Args:
|
|
315
|
-
* - `apiKey` — the project API key (`phc_…`)
|
|
316
|
-
* - `personalApiKey` — a feature flags secure API key (`phs_…`, recommended) or personal API
|
|
317
|
-
* key (`phx_…`) with feature-flag read access; local eval is disabled if missing
|
|
318
|
-
* - `host` — optional, defaults to `https://us.i.posthog.com`
|
|
341
|
+
* `flagDefinitions` table. Called automatically by the cron registered in `crons.ts` when
|
|
342
|
+
* `POSTHOG_PERSONAL_API_KEY` is set, and also exposed publicly so the client's
|
|
343
|
+
* `reloadFeatureFlags(ctx)` method (parity with `posthog-node`) can trigger an on-demand refresh.
|
|
319
344
|
*/
|
|
320
345
|
export const refreshFlagDefinitions = action({
|
|
321
|
-
args: {
|
|
322
|
-
|
|
323
|
-
personalApiKey
|
|
324
|
-
host: v.optional(v.string()),
|
|
325
|
-
},
|
|
326
|
-
handler: async (ctx, args) => {
|
|
327
|
-
const projectApiKey = args.apiKey.trim()
|
|
328
|
-
const personalApiKey = args.personalApiKey.trim()
|
|
329
|
-
const host = (args.host?.trim() || '').replace(/\/$/, '') || 'https://us.i.posthog.com'
|
|
346
|
+
args: {},
|
|
347
|
+
handler: async (ctx) => {
|
|
348
|
+
const { projectToken, host, personalApiKey } = readConfig()
|
|
330
349
|
|
|
331
|
-
if (!
|
|
332
|
-
//
|
|
333
|
-
// (typically a cron) can surface it cleanly.
|
|
350
|
+
if (!projectToken || !personalApiKey) {
|
|
351
|
+
// The cron registers unconditionally (see `crons.ts`); this is its runtime gate.
|
|
334
352
|
return { status: 'skipped' as const, reason: 'missing-keys' as const }
|
|
335
353
|
}
|
|
336
354
|
|
|
337
355
|
const etag = await ctx.runQuery(internal.lib._getCurrentEtag, {})
|
|
338
356
|
|
|
339
|
-
const url = `${host}/flags/definitions?token=${
|
|
357
|
+
const url = `${host.replace(/\/$/, '')}/flags/definitions?token=${projectToken}&send_cohorts`
|
|
340
358
|
const headers: Record<string, string> = {
|
|
341
359
|
'Content-Type': 'application/json',
|
|
342
360
|
Authorization: `Bearer ${personalApiKey}`,
|
|
@@ -419,7 +437,7 @@ export const refreshFlagDefinitions = action({
|
|
|
419
437
|
if (looksCacheCold) {
|
|
420
438
|
const existing = await ctx.runQuery(api.lib.getFlagDefinitions, {})
|
|
421
439
|
const STALE_AFTER_MS = 5 * 60 * 1000
|
|
422
|
-
if (existing === null) {
|
|
440
|
+
if (existing.fetchedAt === null) {
|
|
423
441
|
// No prior cache — write an empty snapshot so subsequent reads are deterministic and
|
|
424
442
|
// the UI shows "no flags" instead of "loading".
|
|
425
443
|
await ctx.runMutation(internal.lib._setFlagDefinitions, {
|
|
@@ -449,7 +467,7 @@ export const refreshFlagDefinitions = action({
|
|
|
449
467
|
}
|
|
450
468
|
|
|
451
469
|
console.warn(
|
|
452
|
-
`[PostHog] Unexpected status ${response.status} fetching flag definitions from ${url.replace(
|
|
470
|
+
`[PostHog] Unexpected status ${response.status} fetching flag definitions from ${url.replace(projectToken, '<token>')}. ` +
|
|
453
471
|
`Response body: ${bodyText}`
|
|
454
472
|
)
|
|
455
473
|
return { status: 'error' as const, reason: 'unexpected-status' as const }
|
package/src/component/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '
|
|
1
|
+
export const version = '2.0.1'
|