rename EVENT_INGEST_KEY -> PUBLISHABLE_KEY
The value is a pk- prefixed PUBLISHABLE key (the Stripe vocabulary, and what the build-time gate already tests for). EVENT_INGEST_KEY hid that. One substitution covers all three spellings, since the framework prefixes wrap the same token: EVENT_INGEST_KEY -> PUBLISHABLE_KEY, NEXT_PUBLIC_* and VITE_* follow. KMS already carries deploy/PUBLISHABLE_KEY (env prod) with the same value, written and read back first, so no build can reach a name that does not exist yet. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
@@ -6,8 +6,8 @@ name: deploy
|
||||
#
|
||||
# The reason is one capability, not tidiness. ui.hanzo.ai is a static export, so
|
||||
# its ingest key has to be BAKED IN at build; the key lives in KMS as
|
||||
# `deploy/EVENT_INGEST_KEY`, and hanzoai/ci is the only lane that reads it and
|
||||
# passes it as `--build-arg EVENT_INGEST_KEY=…`. The `docker build -t "$image" .`
|
||||
# `deploy/PUBLISHABLE_KEY`, and hanzoai/ci is the only lane that reads it and
|
||||
# passes it as `--build-arg PUBLISHABLE_KEY=…`. The `docker build -t "$image" .`
|
||||
# below cannot — there is no KMS hop in it and nowhere to put the value — so any
|
||||
# image it produced would ship a client with no key at all, and cloud answers
|
||||
# `401 ingest_key_required` for an unattributed write. With the Dockerfile now
|
||||
|
||||
+12
-12
@@ -16,12 +16,12 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
||||
# is safe in a public bundle; the deployment that builds decides which org the
|
||||
# site reports as.
|
||||
#
|
||||
# ONE name, end to end: KMS holds `deploy/EVENT_INGEST_KEY`, hanzo.yml declares it
|
||||
# ONE name, end to end: KMS holds `deploy/PUBLISHABLE_KEY`, hanzo.yml declares it
|
||||
# as this image's build_secret, and the KMS name IS the build-arg name. NEXT_PUBLIC_
|
||||
# is added HERE because that prefix is what makes Next inline it — the app reads
|
||||
# process.env.NEXT_PUBLIC_EVENT_INGEST_KEY.
|
||||
# process.env.NEXT_PUBLIC_PUBLISHABLE_KEY.
|
||||
#
|
||||
# Do NOT re-declare `ARG NEXT_PUBLIC_EVENT_INGEST_KEY` after the ENV below. A later
|
||||
# Do NOT re-declare `ARG NEXT_PUBLIC_PUBLISHABLE_KEY` after the ENV below. A later
|
||||
# ARG of the same name shadows the ENV with its own (empty) default, and the build
|
||||
# stays green while the bundle ships blank — which is exactly how hanzo.chat 1.0.58
|
||||
# shipped a keyless site from a fully green run.
|
||||
@@ -39,12 +39,12 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
||||
# so requiring the current `pk-` shape refuses the stale one outright.
|
||||
#
|
||||
# Neither failure is visible from outside the artifact, so refuse the artifact.
|
||||
ARG EVENT_INGEST_KEY
|
||||
ENV NEXT_PUBLIC_EVENT_INGEST_KEY=$EVENT_INGEST_KEY
|
||||
RUN case "$EVENT_INGEST_KEY" in \
|
||||
ARG PUBLISHABLE_KEY
|
||||
ENV NEXT_PUBLIC_PUBLISHABLE_KEY=$PUBLISHABLE_KEY
|
||||
RUN case "$PUBLISHABLE_KEY" in \
|
||||
pk-*) : ;; \
|
||||
'') echo "EVENT_INGEST_KEY is empty - pass --build-arg EVENT_INGEST_KEY=<pk-...> (KMS deploy/EVENT_INGEST_KEY, env prod)" >&2; exit 1 ;; \
|
||||
*) echo "EVENT_INGEST_KEY is not a publishable key (expected a pk- prefix)" >&2; exit 1 ;; \
|
||||
'') echo "PUBLISHABLE_KEY is empty - pass --build-arg PUBLISHABLE_KEY=<pk-...> (KMS deploy/PUBLISHABLE_KEY, env prod)" >&2; exit 1 ;; \
|
||||
*) echo "PUBLISHABLE_KEY is not a publishable key (expected a pk- prefix)" >&2; exit 1 ;; \
|
||||
esac
|
||||
# 300+ prerendered pages; the default heap is not enough.
|
||||
ENV NODE_OPTIONS=--max-old-space-size=8192
|
||||
@@ -71,16 +71,16 @@ RUN cd pkgs/event && pnpm build
|
||||
#
|
||||
# ...and then PROVES the key reached the client bundle. The gate above proves a
|
||||
# key was PASSED; only this proves it was INLINED. Those are different failures:
|
||||
# a rename on either side of `process.env.NEXT_PUBLIC_EVENT_INGEST_KEY` leaves the
|
||||
# a rename on either side of `process.env.NEXT_PUBLIC_PUBLISHABLE_KEY` leaves the
|
||||
# build-arg intact and the bundle keyless, and a static export cannot report that
|
||||
# at runtime because there is no runtime.
|
||||
#
|
||||
# `&&`, never `;` — a `;` chain returns the LAST command's status, so a failed
|
||||
# build followed by a passing grep exits 0 and the image is published.
|
||||
RUN cd app && pnpm build && \
|
||||
if [ -z "${NEXT_PUBLIC_EVENT_INGEST_KEY}" ]; then \
|
||||
echo "ERROR: NEXT_PUBLIC_EVENT_INGEST_KEY is empty after a successful build." >&2; exit 1; \
|
||||
elif grep -rqF "${NEXT_PUBLIC_EVENT_INGEST_KEY}" out; then \
|
||||
if [ -z "${NEXT_PUBLIC_PUBLISHABLE_KEY}" ]; then \
|
||||
echo "ERROR: NEXT_PUBLIC_PUBLISHABLE_KEY is empty after a successful build." >&2; exit 1; \
|
||||
elif grep -rqF "${NEXT_PUBLIC_PUBLISHABLE_KEY}" out; then \
|
||||
echo "Build OK - ingest key inlined into app/out, verified"; \
|
||||
else \
|
||||
echo "ERROR: key supplied but NOT present in app/out - ui would ship unattributed" >&2; exit 1; \
|
||||
|
||||
@@ -474,7 +474,7 @@ whole setup, and every click / change / submit / route change inside the tree
|
||||
reaches `POST /v1/event` named by the component it happened on:
|
||||
|
||||
```tsx
|
||||
<Hanzo analytics={{ product: 'console', ingestKey: process.env.NEXT_PUBLIC_EVENT_INGEST_KEY }}>
|
||||
<Hanzo analytics={{ product: 'console', ingestKey: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY }}>
|
||||
```
|
||||
|
||||
Four packages, one of each concern, no duplication:
|
||||
|
||||
@@ -13,9 +13,9 @@ import { createAnalytics } from "@hanzo/event"
|
||||
// reports as the org that deployed it; without one cloud answers
|
||||
// `401 ingest_key_required` and every logged-out pageview is dropped in silence.
|
||||
//
|
||||
// NEXT_PUBLIC_EVENT_INGEST_KEY is the ONE name, and it is the name the fleet
|
||||
// already carries end to end: KMS holds `deploy/EVENT_INGEST_KEY`, the Dockerfile
|
||||
// takes it as the EVENT_INGEST_KEY build-arg and re-exports it with the
|
||||
// NEXT_PUBLIC_PUBLISHABLE_KEY is the ONE name, and it is the name the fleet
|
||||
// already carries end to end: KMS holds `deploy/PUBLISHABLE_KEY`, the Dockerfile
|
||||
// takes it as the PUBLISHABLE_KEY build-arg and re-exports it with the
|
||||
// NEXT_PUBLIC_ prefix that makes Next inline it.
|
||||
//
|
||||
// This file used to read NEXT_PUBLIC_HANZO_INGEST_KEY — a spelling nothing in KMS
|
||||
@@ -31,5 +31,5 @@ import { createAnalytics } from "@hanzo/event"
|
||||
// resolves to undefined in a static export.
|
||||
export const analytics = createAnalytics({
|
||||
product: "site",
|
||||
ingestKey: process.env.NEXT_PUBLIC_EVENT_INGEST_KEY,
|
||||
ingestKey: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
|
||||
})
|
||||
|
||||
@@ -75,7 +75,7 @@ images:
|
||||
dockerfile: Dockerfile
|
||||
repo: ghcr.io/hanzoai/ui
|
||||
platforms: [linux/amd64]
|
||||
build_secrets: [EVENT_INGEST_KEY]
|
||||
build_secrets: [PUBLISHABLE_KEY]
|
||||
|
||||
# No `deploy:` — ON PURPOSE. hanzoai/universe names what is live, in
|
||||
# charts/app/values/hanzo/ui.yaml, and the in-cluster reconcile restores it — so a
|
||||
|
||||
+3
-3
@@ -119,7 +119,7 @@ registerField('rating', { Display: MyStars, Input: MyStarPicker })
|
||||
```tsx
|
||||
import { Hanzo } from '@hanzo/ui'
|
||||
|
||||
<Hanzo analytics={{ product: 'console', ingestKey: process.env.NEXT_PUBLIC_EVENT_INGEST_KEY }}>
|
||||
<Hanzo analytics={{ product: 'console', ingestKey: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY }}>
|
||||
<App />
|
||||
</Hanzo>
|
||||
```
|
||||
@@ -183,8 +183,8 @@ in **both** directions, because that is what explicit means. Beyond that:
|
||||
|
||||
`ingestKey` is a publishable `pk-…` — write-only, safe in a bundle, minted per
|
||||
org with `POST /v1/keys {"type":"publishable"}`. Omit it and the client reads
|
||||
`NEXT_PUBLIC_EVENT_INGEST_KEY` from the build env, which is the spelling the
|
||||
fleet already carries end to end (KMS holds `deploy/EVENT_INGEST_KEY`; the
|
||||
`NEXT_PUBLIC_PUBLISHABLE_KEY` from the build env, which is the spelling the
|
||||
fleet already carries end to end (KMS holds `deploy/PUBLISHABLE_KEY`; the
|
||||
Dockerfile takes it as a build-arg and re-exports it with the `NEXT_PUBLIC_`
|
||||
prefix Next needs to inline it). A surface with no key reports only for whoever
|
||||
is signed in and silently drops every logged-out visitor — the door refuses an
|
||||
|
||||
@@ -239,7 +239,7 @@ Verified against the trees at the time of writing. ✅ = emitting, ❌ = not emi
|
||||
no org today; chat funnels are person-scoped only.
|
||||
5. **Logged-out reach depends on a publishable key.** Anonymous events need
|
||||
`ingestKey` (`pk_…`, write-only) or they fail closed at the door.
|
||||
`NEXT_PUBLIC_HANZO_INGEST_KEY` (hanzo.ai), `NEXT_PUBLIC_EVENT_INGEST_KEY`
|
||||
`NEXT_PUBLIC_HANZO_INGEST_KEY` (hanzo.ai), `NEXT_PUBLIC_PUBLISHABLE_KEY`
|
||||
(hanzo.app), `VITE_HANZO_INGEST_KEY` (hanzo.chat) are read but must be
|
||||
provisioned per org via `POST /v1/ingest/keys`. Config, not code.
|
||||
|
||||
|
||||
@@ -360,26 +360,26 @@ describe('Analytics capture', () => {
|
||||
// The failure this closes is silent: a surface with no key attributes nothing
|
||||
// for a logged-out visitor, the door refuses the write, and the page shows no
|
||||
// sign of it. The key must resolve from the env exactly as the DSN does.
|
||||
process.env.NEXT_PUBLIC_EVENT_INGEST_KEY = 'pk-live-from-env'
|
||||
process.env.NEXT_PUBLIC_PUBLISHABLE_KEY = 'pk-live-from-env'
|
||||
try {
|
||||
const a = mk() // no key in config
|
||||
a.capture('x')
|
||||
a.flush(true)
|
||||
expect(tx.sent[0].ingestKey).toBe('pk-live-from-env')
|
||||
} finally {
|
||||
delete process.env.NEXT_PUBLIC_EVENT_INGEST_KEY
|
||||
delete process.env.NEXT_PUBLIC_PUBLISHABLE_KEY
|
||||
}
|
||||
})
|
||||
|
||||
it('prefers an explicit ingest key over the build env', () => {
|
||||
process.env.NEXT_PUBLIC_EVENT_INGEST_KEY = 'pk-live-from-env'
|
||||
process.env.NEXT_PUBLIC_PUBLISHABLE_KEY = 'pk-live-from-env'
|
||||
try {
|
||||
const a = mk({ ingestKey: 'pk-live-explicit' })
|
||||
a.capture('x')
|
||||
a.flush(true)
|
||||
expect(tx.sent[0].ingestKey).toBe('pk-live-explicit')
|
||||
} finally {
|
||||
delete process.env.NEXT_PUBLIC_EVENT_INGEST_KEY
|
||||
delete process.env.NEXT_PUBLIC_PUBLISHABLE_KEY
|
||||
}
|
||||
})
|
||||
|
||||
@@ -394,7 +394,7 @@ describe('Analytics capture', () => {
|
||||
// The leak this closes: one console bundle is served to several brands, and a
|
||||
// pk- names ONE org. If an env-sourced key displaced the bearer, every
|
||||
// signed-in user's events would re-file under whichever org minted the key.
|
||||
process.env.NEXT_PUBLIC_EVENT_INGEST_KEY = 'pk-live-one-org'
|
||||
process.env.NEXT_PUBLIC_PUBLISHABLE_KEY = 'pk-live-one-org'
|
||||
try {
|
||||
const a = mk({ getToken: () => 'jwt-of-a-real-person' })
|
||||
a.capture('x')
|
||||
@@ -402,12 +402,12 @@ describe('Analytics capture', () => {
|
||||
expect(tx.sent[0].token).toBe('jwt-of-a-real-person')
|
||||
expect(tx.sent[0].ingestKey).toBeUndefined()
|
||||
} finally {
|
||||
delete process.env.NEXT_PUBLIC_EVENT_INGEST_KEY
|
||||
delete process.env.NEXT_PUBLIC_PUBLISHABLE_KEY
|
||||
}
|
||||
})
|
||||
|
||||
it('an anonymous visitor still rides the key', () => {
|
||||
process.env.NEXT_PUBLIC_EVENT_INGEST_KEY = 'pk-live-one-org'
|
||||
process.env.NEXT_PUBLIC_PUBLISHABLE_KEY = 'pk-live-one-org'
|
||||
try {
|
||||
const a = mk({ getToken: () => undefined }) // logged out
|
||||
a.capture('x')
|
||||
@@ -415,7 +415,7 @@ describe('Analytics capture', () => {
|
||||
expect(tx.sent[0].ingestKey).toBe('pk-live-one-org')
|
||||
expect(tx.sent[0].token).toBeUndefined()
|
||||
} finally {
|
||||
delete process.env.NEXT_PUBLIC_EVENT_INGEST_KEY
|
||||
delete process.env.NEXT_PUBLIC_PUBLISHABLE_KEY
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -241,9 +241,9 @@ export class Analytics {
|
||||
// The publishable key resolves the SAME way the DSN below does: an explicit
|
||||
// config wins, else the inlined build-time env.
|
||||
//
|
||||
// NEXT_PUBLIC_EVENT_INGEST_KEY is that env, and it is the name the fleet
|
||||
// ALREADY carries end to end — KMS holds deploy/EVENT_INGEST_KEY, each
|
||||
// Dockerfile takes it as the EVENT_INGEST_KEY build-arg and re-exports it
|
||||
// NEXT_PUBLIC_PUBLISHABLE_KEY is that env, and it is the name the fleet
|
||||
// ALREADY carries end to end — KMS holds deploy/PUBLISHABLE_KEY, each
|
||||
// Dockerfile takes it as the PUBLISHABLE_KEY build-arg and re-exports it
|
||||
// with the NEXT_PUBLIC_ prefix Next needs to inline it. Reading anything
|
||||
// else here would add a fourth spelling of one value.
|
||||
//
|
||||
@@ -253,7 +253,7 @@ export class Analytics {
|
||||
// unattributed write is refused (401 ingest_key_required), which is silent
|
||||
// in the page and invisible until you read the warehouse and find the host
|
||||
// missing entirely.
|
||||
ingestKey: config.ingestKey ?? readEnv('NEXT_PUBLIC_EVENT_INGEST_KEY'),
|
||||
ingestKey: config.ingestKey ?? readEnv('NEXT_PUBLIC_PUBLISHABLE_KEY'),
|
||||
}
|
||||
this.transport = config.transport ?? new DefaultTransport()
|
||||
// Error plane, most specific source first: an explicit DSN wins, then the
|
||||
|
||||
@@ -120,11 +120,11 @@ export interface AnalyticsConfig {
|
||||
* a reading principal — so it is safe to ship in a bundle. Mint one per org with
|
||||
* POST /v1/keys {"type":"publishable"}.
|
||||
*
|
||||
* Omit it and the client reads NEXT_PUBLIC_EVENT_INGEST_KEY from the inlined
|
||||
* Omit it and the client reads NEXT_PUBLIC_PUBLISHABLE_KEY from the inlined
|
||||
* build env, the same way `dsn` falls back — so a surface declares BOTH planes
|
||||
* in its build and neither needs code to switch on. That is the ONE spelling
|
||||
* the fleet already carries: KMS holds deploy/EVENT_INGEST_KEY, and each
|
||||
* Dockerfile takes EVENT_INGEST_KEY as a build-arg and re-exports it with the
|
||||
* the fleet already carries: KMS holds deploy/PUBLISHABLE_KEY, and each
|
||||
* Dockerfile takes PUBLISHABLE_KEY as a build-arg and re-exports it with the
|
||||
* NEXT_PUBLIC_ prefix that makes Next inline it.
|
||||
*
|
||||
* A surface with no key at all still reports for whoever is SIGNED IN (the
|
||||
|
||||
Reference in New Issue
Block a user