@hogsend/plugin-posthog 0.19.0 → 0.21.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/package.json +2 -2
- package/src/index.ts +2 -1
- package/src/properties.ts +27 -13
- package/src/provider.ts +18 -5
- package/src/types.ts +17 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hogsend/plugin-posthog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"posthog-node": "^5.35.1",
|
|
26
|
-
"@hogsend/core": "^0.
|
|
26
|
+
"@hogsend/core": "^0.21.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"ioredis": ">=5.0.0"
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { captureEvent } from "./capture.js";
|
|
2
|
-
export { createPostHogClient } from "./client.js";
|
|
2
|
+
export { createPostHogClient, DEFAULT_HOST } from "./client.js";
|
|
3
3
|
export { derivePrivateHost, getPersonProperties } from "./properties.js";
|
|
4
4
|
export { createPostHogProvider } from "./provider.js";
|
|
5
5
|
export { createPostHogService } from "./service.js";
|
|
@@ -9,6 +9,7 @@ export type {
|
|
|
9
9
|
PersonPropertiesCache,
|
|
10
10
|
PersonPropertiesConfig,
|
|
11
11
|
PersonPropertiesWrite,
|
|
12
|
+
PostHogAuthTokenAccessor,
|
|
12
13
|
PostHogService,
|
|
13
14
|
PostHogServiceConfig,
|
|
14
15
|
} from "./types.js";
|
package/src/properties.ts
CHANGED
|
@@ -25,14 +25,14 @@ export function derivePrivateHost(host: string): string {
|
|
|
25
25
|
*/
|
|
26
26
|
async function discoverProjectId(opts: {
|
|
27
27
|
privateHost: string;
|
|
28
|
-
|
|
28
|
+
token: string;
|
|
29
29
|
}): Promise<string | undefined> {
|
|
30
30
|
try {
|
|
31
31
|
const response = await fetch(
|
|
32
32
|
new URL("/api/projects/@current/", opts.privateHost).toString(),
|
|
33
33
|
{
|
|
34
34
|
headers: {
|
|
35
|
-
Authorization: `Bearer ${opts.
|
|
35
|
+
Authorization: `Bearer ${opts.token}`,
|
|
36
36
|
Accept: "application/json",
|
|
37
37
|
},
|
|
38
38
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
@@ -46,16 +46,22 @@ async function discoverProjectId(opts: {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/** Per-(host,
|
|
49
|
+
/** Per-(host,credential) one-shot project-id discovery, shared across calls. */
|
|
50
50
|
const projectIdCache = new Map<string, Promise<string | undefined>>();
|
|
51
51
|
|
|
52
52
|
function resolveProjectId(opts: {
|
|
53
53
|
privateHost: string;
|
|
54
|
-
|
|
54
|
+
token: string;
|
|
55
|
+
/**
|
|
56
|
+
* Cache identity, NOT the bearer token: OAuth access tokens rotate every
|
|
57
|
+
* ~10h, so keying on `token` would re-discover after every refresh. Callers
|
|
58
|
+
* pass the (stable) personal key or a fixed "oauth" marker.
|
|
59
|
+
*/
|
|
60
|
+
cacheKey: string;
|
|
55
61
|
projectId?: string;
|
|
56
62
|
}): Promise<string | undefined> {
|
|
57
63
|
if (opts.projectId) return Promise.resolve(opts.projectId);
|
|
58
|
-
const cacheKey = `${opts.privateHost}::${opts.
|
|
64
|
+
const cacheKey = `${opts.privateHost}::${opts.cacheKey}`;
|
|
59
65
|
let pending = projectIdCache.get(cacheKey);
|
|
60
66
|
if (!pending) {
|
|
61
67
|
pending = discoverProjectId(opts).then((id) => {
|
|
@@ -71,11 +77,12 @@ function resolveProjectId(opts: {
|
|
|
71
77
|
/**
|
|
72
78
|
* Person-property READ via PostHog's private API.
|
|
73
79
|
*
|
|
74
|
-
* Requires a
|
|
75
|
-
* is write-only by design (it ships in browser bundles) and can never
|
|
76
|
-
*
|
|
77
|
-
* the
|
|
78
|
-
*
|
|
80
|
+
* Requires a privileged credential (scope `person:read`) — the `phc_` project
|
|
81
|
+
* key is write-only by design (it ships in browser bundles) and can never
|
|
82
|
+
* read. An OAuth token (via `getAuthToken`) is preferred; `personalApiKey` is
|
|
83
|
+
* the fallback. With NEITHER configured this resolves `{}` immediately (reads
|
|
84
|
+
* disabled); the engine's timezone fallbacks (contact properties → client
|
|
85
|
+
* default) take over. All upstream errors also soft-fail to `{}`.
|
|
79
86
|
*
|
|
80
87
|
* The private API lives on the APP host (`eu.posthog.com`), not the
|
|
81
88
|
* ingestion host (`eu.i.posthog.com`) — derived via {@link derivePrivateHost}.
|
|
@@ -87,7 +94,7 @@ export async function getPersonProperties(opts: {
|
|
|
87
94
|
}): Promise<Record<string, unknown>> {
|
|
88
95
|
const { config, distinctId, cache } = opts;
|
|
89
96
|
|
|
90
|
-
if (!config.personalApiKey) return {};
|
|
97
|
+
if (!config.personalApiKey && !config.getAuthToken) return {};
|
|
91
98
|
|
|
92
99
|
if (cache) {
|
|
93
100
|
try {
|
|
@@ -105,9 +112,16 @@ export async function getPersonProperties(opts: {
|
|
|
105
112
|
let properties: Record<string, unknown> = {};
|
|
106
113
|
|
|
107
114
|
try {
|
|
115
|
+
// OAuth preferred, personal key fallback — a revoked/failed OAuth
|
|
116
|
+
// credential degrades to the personal key for free.
|
|
117
|
+
const oauthToken = config.getAuthToken ? await config.getAuthToken() : null;
|
|
118
|
+
const token = oauthToken ?? config.personalApiKey;
|
|
119
|
+
if (!token) return {};
|
|
120
|
+
|
|
108
121
|
const projectId = await resolveProjectId({
|
|
109
122
|
privateHost,
|
|
110
|
-
|
|
123
|
+
token,
|
|
124
|
+
cacheKey: config.personalApiKey ?? "oauth",
|
|
111
125
|
projectId: config.projectId,
|
|
112
126
|
});
|
|
113
127
|
if (!projectId) return {};
|
|
@@ -120,7 +134,7 @@ export async function getPersonProperties(opts: {
|
|
|
120
134
|
|
|
121
135
|
const response = await fetch(url.toString(), {
|
|
122
136
|
headers: {
|
|
123
|
-
Authorization: `Bearer ${
|
|
137
|
+
Authorization: `Bearer ${token}`,
|
|
124
138
|
Accept: "application/json",
|
|
125
139
|
},
|
|
126
140
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
package/src/provider.ts
CHANGED
|
@@ -16,19 +16,23 @@ import type {
|
|
|
16
16
|
* - **capture + person WRITES** use the public project key (`apiKey`) — person
|
|
17
17
|
* writes ride the capture pipeline as `$set`/`$set_once`, so propagation
|
|
18
18
|
* needs NO extra credential.
|
|
19
|
-
* - **person READS** need
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
22
|
-
*
|
|
19
|
+
* - **person READS** need a privileged credential against the private API
|
|
20
|
+
* host: an engine-injected OAuth accessor (`authToken`, preferred) or
|
|
21
|
+
* `personalApiKey` (a personal API key scoped `person:read`, the fallback).
|
|
22
|
+
* With neither, `capabilities.personReads` is false and reads soft-fail to
|
|
23
|
+
* `{}` — the engine falls back to contact properties for timezone
|
|
24
|
+
* resolution.
|
|
23
25
|
*/
|
|
24
26
|
export function createPostHogProvider(
|
|
25
27
|
config: PostHogServiceConfig,
|
|
26
28
|
): AnalyticsProvider {
|
|
27
29
|
const host = config.host ?? DEFAULT_HOST;
|
|
28
30
|
const client = createPostHogClient({ apiKey: config.apiKey, host });
|
|
31
|
+
const authToken = config.authToken;
|
|
29
32
|
|
|
30
33
|
const propsConfig: PersonPropertiesConfig = {
|
|
31
34
|
personalApiKey: config.personalApiKey,
|
|
35
|
+
getAuthToken: authToken ? () => authToken.getToken() : undefined,
|
|
32
36
|
host,
|
|
33
37
|
privateHost: config.privateHost,
|
|
34
38
|
projectId: config.projectId,
|
|
@@ -46,8 +50,17 @@ export function createPostHogProvider(
|
|
|
46
50
|
"PostHog capture + person reads/writes (reads need a personal API key).",
|
|
47
51
|
},
|
|
48
52
|
capabilities: {
|
|
49
|
-
|
|
53
|
+
// LIVE getter: the container builds providers at BOOT, but the OAuth
|
|
54
|
+
// credential can be stored at RUNTIME via `hogsend connect posthog`.
|
|
55
|
+
// A getter means every reader (boot nudge, doctor, future Studio
|
|
56
|
+
// status) sees current truth without rebuilding the provider.
|
|
57
|
+
get personReads() {
|
|
58
|
+
return (
|
|
59
|
+
Boolean(config.personalApiKey) || (authToken?.isAvailable() ?? false)
|
|
60
|
+
);
|
|
61
|
+
},
|
|
50
62
|
personWrites: true,
|
|
63
|
+
oauth: true,
|
|
51
64
|
},
|
|
52
65
|
|
|
53
66
|
async getPersonProperties(distinctId: string) {
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { Redis } from "ioredis";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Engine-injected OAuth auth accessor. `getToken()` resolves a live bearer
|
|
5
|
+
* token (or null — degrade); `isAvailable()` is the synchronous best-known
|
|
6
|
+
* credential state, used for live capability reporting. The plugin stays a
|
|
7
|
+
* dumb wire: it neither refreshes nor stores anything.
|
|
8
|
+
*/
|
|
9
|
+
export interface PostHogAuthTokenAccessor {
|
|
10
|
+
getToken(): Promise<string | null>;
|
|
11
|
+
isAvailable(): boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
export interface PostHogServiceConfig {
|
|
4
15
|
/** Project API key (`phc_…`) — capture/flags. Public + WRITE-ONLY by design. */
|
|
5
16
|
apiKey: string;
|
|
@@ -28,6 +39,8 @@ export interface PostHogServiceConfig {
|
|
|
28
39
|
projectId?: string;
|
|
29
40
|
redis?: Redis;
|
|
30
41
|
cacheTtlSeconds?: number;
|
|
42
|
+
/** OAuth accessor — preferred over `personalApiKey` when it yields a token. */
|
|
43
|
+
authToken?: PostHogAuthTokenAccessor;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
// The analytics-provider contract now lives in the neutral @hogsend/core
|
|
@@ -41,8 +54,11 @@ export type {
|
|
|
41
54
|
} from "@hogsend/core";
|
|
42
55
|
|
|
43
56
|
export interface PersonPropertiesConfig {
|
|
44
|
-
/** Personal API key —
|
|
57
|
+
/** Personal API key — fallback when OAuth yields no token. Reads are
|
|
58
|
+
* DISABLED (soft-fail `{}`) when BOTH this and `getAuthToken` are absent. */
|
|
45
59
|
personalApiKey?: string;
|
|
60
|
+
/** Async OAuth token resolver (the accessor's getToken, pre-bound). */
|
|
61
|
+
getAuthToken?: () => Promise<string | null>;
|
|
46
62
|
/** Capture/ingestion host (private host is derived from it). */
|
|
47
63
|
host: string;
|
|
48
64
|
/** Private API host override. */
|