@hogsend/cli 0.17.1 → 0.19.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hogsend/cli",
3
- "version": "0.17.1",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "tsup": "^8.5.1",
35
35
  "tsx": "^4.22.4",
36
36
  "vitest": "^4.1.7",
37
- "@hogsend/studio": "^0.17.1",
37
+ "@hogsend/studio": "^0.19.0",
38
38
  "@repo/typescript-config": "0.0.0"
39
39
  },
40
40
  "engines": {
@@ -44,8 +44,8 @@
44
44
  "@clack/prompts": "^1.5.0",
45
45
  "better-auth": "^1.6.11",
46
46
  "picocolors": "^1.1.1",
47
- "@hogsend/db": "^0.17.1",
48
- "@hogsend/engine": "^0.17.1"
47
+ "@hogsend/db": "^0.19.0",
48
+ "@hogsend/engine": "^0.19.0"
49
49
  },
50
50
  "scripts": {
51
51
  "prebuild": "node scripts/bundle-studio.mjs",
@@ -149,7 +149,10 @@ orchestration:
149
149
  - **`getPostHog()`** — `import { getPostHog } from "@hogsend/engine"` for the raw
150
150
  PostHog service (a fire-and-forget escape hatch). For fanning lifecycle data out
151
151
  to product/data tools, prefer an outbound DESTINATION (see above) — it delivers
152
- durably and is vendor-neutral.
152
+ durably and is vendor-neutral. Note: capture and `$set` person WRITES use the
153
+ `phc_` project key; person READS (`getPersonProperties`) additionally need
154
+ `POSTHOG_PERSONAL_API_KEY` (the project key is write-only by PostHog's design)
155
+ and soft-fail to `{}` without it.
153
156
  - **SMS / push / Slack** — plain functions you import, never on `ctx`.
154
157
  - There is **no `ctx.db`, no `ctx.sendEmail`, no `ctx.hatchet`** surfaced to
155
158
  consumer journeys. If you reach for one of those, you are modelling it wrong —
@@ -89,7 +89,7 @@ await hs.events.send({ // POST /v1/events
89
89
  eventProperties: { source: "landing" }, // stored ON the event
90
90
  contactProperties: { country: "GB" }, // merged onto the CONTACT
91
91
  idempotencyKey: "evt_abc",
92
- }); // -> { stored, exits, listsError? }
92
+ }); // -> { stored, exits, contactKey?, listsError? }
93
93
  hs.events.track(/* … */); // alias of events.send
94
94
 
95
95
  // Emails ------------------------------------------------------------------
@@ -185,13 +185,17 @@ same split is exposed by the CLI as `--prop` (event) vs `--contact-prop`
185
185
  ## The 202 + `listsError` warning
186
186
 
187
187
  `POST /v1/events` returns **202 Accepted** (the event is durably stored and
188
- queued for routing), NOT 200. The result is `{ stored, exits }` plus an optional
189
- `listsError`:
188
+ queued for routing), NOT 200. The result is `{ stored, exits, contactKey }`
189
+ plus an optional `listsError`:
190
190
 
191
191
  - `stored` — `true` once the event row is written (`false` only on a dedup via
192
192
  `idempotencyKey`).
193
193
  - `exits` — the `{ journeyId, stateId, exited }[]` from evaluating active
194
194
  journeys' `exitOn` rules for this user.
195
+ - `contactKey` — the contact's canonical key (engine ≥0.18): the same key
196
+ outbound destinations emit as `userId` and `hs_t` identity tokens resolve
197
+ to. Hand it to your analytics `identify()` to join the session to the
198
+ contact's person — it carries no PII.
195
199
  - **`listsError?`** — present ONLY when the event was ingested fine but the
196
200
  (non-atomic, post-ingest) `lists` membership write failed. **The event itself
197
201
  is durably stored** — this is a soft warning surfaced on the 202, not a 400.
@@ -73,10 +73,19 @@ dashboard at `http://localhost:8888`, login `admin@example.com` / `Admin123!!`.)
73
73
  All commented-out in `.env.example` — add only what you use:
74
74
 
75
75
  ```bash
76
- # PostHog person properties + event capture (no-op if unset).
76
+ # PostHog event capture + person property WRITES (no-op if unset).
77
77
  POSTHOG_API_KEY=phc_...
78
78
  POSTHOG_HOST=https://us.i.posthog.com
79
79
 
80
+ # PostHog person property READS (timezone resolution, property conditions).
81
+ # The phc_ key is write-only by PostHog's design — reads need a PERSONAL API
82
+ # key scoped person:read. Without it reads soft-fail to contact properties.
83
+ POSTHOG_PERSONAL_API_KEY=...
84
+ # Project id for the private API — discovered automatically when unset.
85
+ # POSTHOG_PROJECT_ID=12345
86
+ # Private API host — derived (eu.i.posthog.com → eu.posthog.com) when unset.
87
+ # POSTHOG_PRIVATE_HOST=https://us.posthog.com
88
+
80
89
  # Verify incoming PostHog webhooks (POST /v1/webhooks/posthog).
81
90
  POSTHOG_WEBHOOK_SECRET=...
82
91
 
@@ -92,6 +101,12 @@ Notes:
92
101
 
93
102
  - **PostHog is fully optional.** Without `POSTHOG_API_KEY`, person-property
94
103
  fetches and event captures are no-ops — journeys still run.
104
+ - **Two PostHog credentials, by PostHog's design.** The `phc_` project key is
105
+ public (it ships in browser bundles) so PostHog makes it WRITE-only: capture
106
+ and `$set` person writes work, reads never will. Person READS (per-user
107
+ timezone resolution) need `POSTHOG_PERSONAL_API_KEY` — a personal API key
108
+ scoped `person:read`, created in PostHog → Settings → Personal API keys.
109
+ `hogsend doctor` warns when capture is configured without it.
95
110
  - **Webhook secrets are per-source.** Only set the secret for a webhook source
96
111
  you've actually registered (see the consumer's `src/webhook-sources`).
97
112
  - **`ADMIN_API_KEY` gates `/v1/admin/*`.** Set it in prod if you want to drive
@@ -1,4 +1,5 @@
1
1
  import { parseArgs } from "node:util";
2
+ import { loadDotEnv } from "../lib/config.js";
2
3
  import { isHttpError } from "../lib/http.js";
3
4
  import { color } from "../lib/output.js";
4
5
  import { skillsStaleness } from "../lib/skills.js";
@@ -22,6 +23,34 @@ function skillsNudge(ctx: CommandContext): void {
22
23
  );
23
24
  }
24
25
 
26
+ /**
27
+ * Best-effort nudge: PostHog capture configured (`POSTHOG_API_KEY` in the
28
+ * cwd's `.env` or process env) without `POSTHOG_PERSONAL_API_KEY` means
29
+ * person READS are silently disabled — the phc_ project key is write-only by
30
+ * PostHog's design, so timezone resolution falls back to contact properties.
31
+ * Warn-not-fail: capture and person writes still work.
32
+ */
33
+ function analyticsNudge(ctx: CommandContext): void {
34
+ if (ctx.json) return;
35
+ const dotenv = loadDotEnv(process.cwd());
36
+ const captureKey = process.env.POSTHOG_API_KEY ?? dotenv.POSTHOG_API_KEY;
37
+ const personalKey =
38
+ process.env.POSTHOG_PERSONAL_API_KEY ?? dotenv.POSTHOG_PERSONAL_API_KEY;
39
+ if (!captureKey || personalKey) return;
40
+ ctx.out.note(
41
+ [
42
+ "POSTHOG_API_KEY is set without POSTHOG_PERSONAL_API_KEY — person",
43
+ "property READS are disabled (the phc_ project key is write-only by",
44
+ "PostHog's design), so per-user timezone resolution falls back to",
45
+ "contact properties. Capture and person WRITES are unaffected.",
46
+ "",
47
+ `Fix: create a personal API key scoped ${color.cyan("person:read")} and set ${color.cyan("POSTHOG_PERSONAL_API_KEY")}.`,
48
+ `Docs: ${color.cyan("https://hogsend.com/docs/guides/analytics-access")}`,
49
+ ].join("\n"),
50
+ "PostHog person reads disabled",
51
+ );
52
+ }
53
+
25
54
  const usage = `hogsend doctor [--url <baseUrl>] [--admin-key <key>] [--json]
26
55
 
27
56
  Probe a running Hogsend instance via GET /v1/health and report its health:
@@ -221,6 +250,7 @@ async function run(ctx: CommandContext): Promise<void> {
221
250
  ctx.out.note(lines.join("\n"), "Doctor");
222
251
 
223
252
  skillsNudge(ctx);
253
+ analyticsNudge(ctx);
224
254
 
225
255
  if (ok) {
226
256
  ctx.out.outro(color.green("doctor: ok"));