Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/website/public/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Angular Agent Framework v0.0.28
# Angular Agent Framework v0.0.29

Agent UI primitives and LangGraph streaming adapters for Angular.

Expand Down
2 changes: 1 addition & 1 deletion apps/website/public/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Angular Agent Framework v0.0.28
# Angular Agent Framework v0.0.29

Agent UI primitives and LangGraph streaming adapters for Angular.

Expand Down
79 changes: 79 additions & 0 deletions apps/website/src/app/api/ingest/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { PostHog } from 'posthog-node';
import { NextRequest, NextResponse } from 'next/server';
import { normalizePostHogHost, toSafeAnalyticsString } from '../../../lib/analytics/properties';

const PUBLIC_INGEST_KEY = 'phc_public_cacheplane_telemetry';

interface TelemetryIngestPayload {
key?: unknown;
distinctId?: unknown;
event?: unknown;
properties?: unknown;
}

function getPostHogClient(): PostHog | null {
const token = toSafeAnalyticsString(process.env.NEXT_PUBLIC_POSTHOG_TOKEN, 500);
if (!token) return null;
return new PostHog(token, {
host: normalizePostHogHost(process.env.NEXT_PUBLIC_POSTHOG_HOST),
flushAt: 1,
flushInterval: 0,
});
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function readPayload(value: unknown): {
distinctId: string;
event: string;
properties: Record<string, unknown>;
} | null {
if (!isRecord(value)) return null;
const payload = value as TelemetryIngestPayload;
if (payload.key !== PUBLIC_INGEST_KEY) return null;

const distinctId = toSafeAnalyticsString(payload.distinctId, 200);
const event = toSafeAnalyticsString(payload.event, 100);
if (!distinctId || !event?.startsWith('ngaf:')) return null;

return {
distinctId,
event,
properties: isRecord(payload.properties) ? payload.properties : {},
};
}

export async function POST(req: NextRequest) {
let body: unknown;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
}

const payload = readPayload(body);
if (!payload) return NextResponse.json({ error: 'Invalid telemetry payload' }, { status: 400 });

const posthog = getPostHogClient();
if (!posthog) return NextResponse.json({ error: 'Telemetry ingest is not configured' }, { status: 503 });

try {
posthog.capture({
distinctId: payload.distinctId,
event: payload.event,
properties: {
...payload.properties,
$ip: null,
$process_person_profile: false,
},
});
await posthog.shutdown();
return NextResponse.json({ ok: true }, { status: 202 });
} catch (err) {
console.error('[telemetry-ingest] capture failed:', err);
await posthog.shutdown().catch(() => undefined);
return NextResponse.json({ error: 'Telemetry ingest failed' }, { status: 502 });
}
}
2 changes: 1 addition & 1 deletion apps/website/src/components/landing/FinalCTA.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function FinalCTA({
subtext = 'Install the framework, read the docs, and have a streaming chat in your app this afternoon.',
primary = DEFAULT_PRIMARY,
secondary = DEFAULT_SECONDARY,
caption = 'MIT · No signup required · No telemetry',
caption = 'MIT · No signup required · App telemetry off by default',
}: FinalCTAProps = {}) {
return (
<Section surface="tinted" ariaLabelledBy="final-cta-heading">
Expand Down
4 changes: 2 additions & 2 deletions apps/website/src/components/landing/Promises.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const PROMISES = [
body: 'Self-host LangGraph + your Angular app. Run it all in your VPC.',
},
{
title: 'No telemetry',
body: 'We don’t collect anything from your app. Not at install, not at runtime.',
title: 'No app telemetry',
body: 'We don’t collect prompts, completions, tool data, or app runtime content by default. Package installs send a minimal opt-out ping.',
},
{
title: 'No model lock-in',
Expand Down
5 changes: 3 additions & 2 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ npx nx release patch

This runs Nx Release in interactive mode, which:

1. Builds all seven publishable projects (preVersionCommand).
1. Builds all seven publishable projects and patches install telemetry into the publishable package manifests (preVersionCommand).
2. Bumps every package.json version (e.g., `0.0.1` → `0.0.2`).
3. Generates `CHANGELOG.md` from commits since the last tag.
4. Creates a git commit `chore(release): publish v0.0.2`.
Expand Down Expand Up @@ -50,8 +50,9 @@ npx nx release publish --groups=publishable
The very first publish ships the version currently on disk (`0.0.1`) — no version bump. `--first-release` skips the "previous tag exists" check and the "package already on registry" check.

```bash
# 1. Build everything
# 1. Build everything and patch install telemetry into publishable manifests
npx nx run-many -t build --projects=chat,langgraph,ag-ui,render,a2ui,licensing,telemetry
node libs/telemetry/scripts/apply-install-telemetry.mjs dist/libs/chat dist/libs/langgraph dist/libs/ag-ui dist/libs/render dist/libs/a2ui dist/libs/licensing

# 2. Generate the initial CHANGELOG, commit, and tag v0.0.1
npx nx release changelog 0.0.1 --first-release
Expand Down
8 changes: 4 additions & 4 deletions docs/gtm/messaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@

**H1:** Ship production agent UIs in Angular.

**Subhead:** Signal-native chat, threads, interrupts, tool progress, and generative UI for LangGraph, AG-UI, and A2UI. MIT-licensed, self-hostable, opt-in telemetry, no React rewrite.
**Subhead:** Signal-native chat, threads, interrupts, tool progress, and generative UI for LangGraph, AG-UI, and A2UI. MIT-licensed, self-hostable, app telemetry off by default, no React rewrite.

**Primary CTA:** `Install @ngaf/chat` (copy-to-clipboard, fires `marketing:cta_click` with `cta_id=hero_install`, `track=developer`).

**Secondary CTA:** `Talk to our engineers` (routes to `/contact?source=home_hero&track=enterprise`, fires `marketing:cta_click` with `cta_id=hero_talk_to_engineers`, `track=enterprise`).

**Proof row:** `MIT · Angular-native Signals · LangGraph + AG-UI · A2UI-compatible · Self-hostable · Opt-in telemetry`
**Proof row:** `MIT · Angular-native Signals · LangGraph + AG-UI · A2UI-compatible · Self-hostable · App telemetry off by default`

**Subline under proof row:** *Not another backend agent runtime. Keep LangGraph, Genkit, Mastra, CrewAI, or your own service. Cacheplane solves the Angular UI layer.*

Expand All @@ -27,12 +27,12 @@ Repeat across the site, comparison pages, and content.
1. **Angular-native, not React-translated.** Signals, DI, OnPush, standalone components, Angular testing patterns, design-system ownership.
2. **Complete agent UI, not just stream plumbing.** Messages, status, errors, tool progress, interrupts, branching/history, thread persistence, reload, fallbacks, tests.
3. **Generative UI that respects the enterprise design system.** Approved components from your design system; no arbitrary code shipping.
4. **Enterprise OSS posture.** MIT, no end-user telemetry, no required cloud, self-hosting, optional paid support/SLA.
4. **Enterprise OSS posture.** MIT, no app/runtime content telemetry by default, no required cloud, self-hosting, optional paid support/SLA.
5. **Production patterns, not demo candy.** Real auth, real backends, observability, error boundaries, fallback strategies, CI/CD, load/chaos patterns, runbooks.

## Risk-cleanup copy changes (Spec 2)

- "No telemetry" → "**Opt-in telemetry**" with link to `libs/telemetry/README.md`.
- "No telemetry" → "**App telemetry off by default**" with link to `libs/telemetry/README.md` for the minimal opt-out package install ping.
- "All Angular versions" (pricing) → **real compatibility matrix** with supported/experimental/planned/unsupported.
- "A2UI v1" → **"A2UI v0.9-compatible"** until v1 is verified.
- "Angular Agent Framework" → **"Agent UI for Angular"** (category sweep, with care for substring overlap per existing memory note).
Expand Down
3 changes: 1 addition & 2 deletions docs/gtm/taxonomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,8 @@ The standard PostHog `$pageview` event is used as-is across all three surfaces.

| Event | When | Surface | Default |
|--------------------------------------|--------------------------------------------|-----------------|--------------|
| `ngaf:postinstall` | `npm install` of an `@ngaf/*` package | Node (script) | **Opt-out** |
| `ngaf:postinstall` | Dependency/global install of a published `@ngaf/*` package | Node (script) | **Opt-out** |
| `ngaf:runtime_instance_created` | Server adapter init | Node | **Opt-out** |
| `ngaf:runtime_request_created` | Server adapter handles a request | Node | **Opt-out** |
| `ngaf:stream_started` | Stream begins | Node | **Opt-out** |
| `ngaf:stream_ended` | Stream ends normally | Node | **Opt-out** |
| `ngaf:stream_errored` | Stream errors | Node | **Opt-out** |
Expand Down
2 changes: 1 addition & 1 deletion gtm.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Operational progress lives in agent runs and PostHog. The repo holds durable str
## 8. Non-goals (current phase)

- We do not compete as a general agent UI framework. We claim the Angular final mile.
- We do not ship telemetry from `@ngaf/*` browser packages by default. Opt-in only. Node-side telemetry honors `DO_NOT_TRACK` and `NGAF_TELEMETRY_DISABLED`; see [libs/telemetry/README.md](libs/telemetry/README.md) for the full contract.
- We do not ship telemetry from `@ngaf/*` browser packages by default. Opt-in only. Node-side package telemetry is minimal and honors `DO_NOT_TRACK`, `npm_config_do_not_track`, and `NGAF_TELEMETRY_DISABLED`; see [libs/telemetry/README.md](libs/telemetry/README.md) for the full contract.
- We do not run paid acquisition until Phase 2 organic baselines exist.
- We do not pursue stars as a vanity metric.
- We do not run A/B positioning experiments in Phase 1. Ship one hero, measure, iterate.
Expand Down
9 changes: 0 additions & 9 deletions libs/ag-ui/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@
"sourceRoot": "libs/ag-ui/src",
"prefix": "lib",
"projectType": "library",
"release": {
"version": {
"manifestRootsToUpdate": [
"{projectRoot}"
],
"currentVersionResolver": "git-tag",
"fallbackCurrentVersionResolver": "disk"
}
},
"tags": [],
"targets": {
"build": {
Expand Down
9 changes: 0 additions & 9 deletions libs/chat/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@
"sourceRoot": "libs/chat/src",
"prefix": "chat",
"projectType": "library",
"release": {
"version": {
"manifestRootsToUpdate": [
"{projectRoot}"
],
"currentVersionResolver": "git-tag",
"fallbackCurrentVersionResolver": "disk"
}
},
"tags": [],
"targets": {
"build": {
Expand Down
9 changes: 0 additions & 9 deletions libs/langgraph/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@
"sourceRoot": "libs/langgraph/src",
"prefix": "lib",
"projectType": "library",
"release": {
"version": {
"manifestRootsToUpdate": [
"{projectRoot}"
],
"currentVersionResolver": "git-tag",
"fallbackCurrentVersionResolver": "disk"
}
},
"tags": [],
"targets": {
"build": {
Expand Down
9 changes: 0 additions & 9 deletions libs/render/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@
"sourceRoot": "libs/render/src",
"prefix": "render",
"projectType": "library",
"release": {
"version": {
"manifestRootsToUpdate": [
"{projectRoot}"
],
"currentVersionResolver": "git-tag",
"fallbackCurrentVersionResolver": "disk"
}
},
"tags": [],
"targets": {
"build": {
Expand Down
28 changes: 18 additions & 10 deletions libs/telemetry/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# @ngaf/telemetry

> Skeleton. Implementation lands in Spec 1 (`analytics-foundation`).
> This README is the **public trust contract**. It's linked from the homepage footer, package READMEs, and the postinstall opt-out notice. The contract is locked here so it doesn't drift.
This README is the public trust contract for `@ngaf/*` telemetry. It is linked
from package install notices and should stay aligned with implementation.

## Imports

Expand Down Expand Up @@ -29,8 +29,8 @@ The single telemetry surface for `@ngaf/*`. It exists so we can answer "how is C
## What is and isn't telemetered

**Telemetered by default (Node, opt-out):**
- `ngaf:postinstall` — fires once per `npm install` of an `@ngaf/*` package. Properties: package name, package version, Node version, OS. No identifiers, no project path, no environment variables.
- `ngaf:runtime_instance_created` — server adapters (LangGraph, AG-UI) call this when they spin up. Properties: which transport, which model provider (string), Angular peer version. **No API keys**, no endpoint hostnames, no user data. Sensitive identifiers are hashed (SHA-256, one-way).
- `ngaf:postinstall` — fires once per dependency/global install of a published `@ngaf/*` package. Properties: package name, package version, Node version, OS, package manager name/version when npm exposes it, sample weight. It uses a per-process anonymous id. No project path, no environment variables, no dependency tree, no installer IP address.
- `ngaf:runtime_instance_created` — server adapters (LangGraph, AG-UI) call this when they spin up. Properties: which transport, which model provider (string), Angular peer version. **No API keys**, no endpoint hostnames, no user data.
- `ngaf:stream_started` / `ngaf:stream_ended` / `ngaf:stream_errored` — per-request lifecycle on server adapters. Properties: provider, model name, duration, error class. No prompts, no completions, no message content.

**Telemetered only on explicit opt-in (Browser):**
Expand All @@ -41,7 +41,6 @@ The single telemetry surface for `@ngaf/*`. It exists so we can answer "how is C
- Message content (user prompts, model completions, tool call inputs/outputs).
- Personally identifiable information beyond `email_domain` on explicit server conversion events on the website.
- API keys, vendor credentials, project paths, environment variables.
- Whatever you've told the SDK to ignore via the redaction config.

## Opt-out

Expand All @@ -50,17 +49,26 @@ Node telemetry is on by default. Three ways to opt out — any one turns it off.
| Method | How |
|--------|-----|
| Cross-vendor env var | `DO_NOT_TRACK=1` or `DO_NOT_TRACK=true` |
| npm config env var | `npm_config_do_not_track=true` |
| Package env var | `NGAF_TELEMETRY_DISABLED=1` or `NGAF_TELEMETRY_DISABLED=true` |
| Programmatic | `import { disableTelemetry } from '@ngaf/telemetry/node'; disableTelemetry();` before any other `@ngaf/*` import |

CI environments (`CI=true`, `GITHUB_ACTIONS=true`, etc.) are auto-detected and treated as opt-out by default.

The postinstall script prints a single line on stdout describing what was sent and how to disable. The line is suppressed in CI.
Local top-level installs are skipped by default. Dependency installs and global installs are eligible unless opted out.

The postinstall script prints a single line on stdout only when the install ping was actually accepted by the ingest endpoint. The line is suppressed in CI.

To inspect the install payload locally, run with `DEBUG=ngaf:telemetry`.

## Opt-in (browser)

Browser telemetry is **off by default** and never fires from the library itself. To enable in your Angular app:

```bash
npm install posthog-js
```

```ts
// app.config.ts (or wherever you bootstrap)
import { provideNgafTelemetry } from '@ngaf/telemetry/browser';
Expand All @@ -82,7 +90,7 @@ If you don't call `provideNgafTelemetry({ enabled: true })`, every telemetry hel
## Sampling

- Default sample rate: **1.0** (100%) at current scale.
- Configurable via `NGAF_TELEMETRY_SAMPLE_RATE` env var (Node) or the `sampleRate` option (Browser).
- Configurable via `NGAF_TELEMETRY_SAMPLE_RATE` env var (Node).
- Every event carries a `sample_weight` property so future de-sampling at query time works correctly.

## Anonymous id strategy
Expand All @@ -96,10 +104,10 @@ If you don't call `provideNgafTelemetry({ enabled: true })`, every telemetry hel
Enterprise users can redirect Node telemetry to their own ingest:

```bash
NGAF_TELEMETRY_INGEST_URL=https://posthog.acme-internal.example.com
NGAF_TELEMETRY_INGEST_URL=https://telemetry.acme-internal.example.com/api/ingest
```

Default ingest (when env var is unset) is a thin proxy on the Cacheplane website (`https://cacheplane.dev/api/ingest`) that forwards to our PostHog project. Source of the proxy lives in `apps/website/src/app/api/ingest/`.
Default ingest (when env var is unset) is a thin proxy on the Cacheplane website (`https://cacheplane.dev/api/ingest`) that accepts the `@ngaf/telemetry` JSON payload and forwards `ngaf:*` events to our PostHog project without forwarding installer IP addresses. Source of the proxy lives in `apps/website/src/app/api/ingest/`.

## Verifying telemetry is silent

Expand All @@ -116,7 +124,7 @@ If this test ever fails, the trust contract has been violated and the build bloc
- Session replay. (Not in Phase 0–1.)
- Cross-session identity stitching.
- Heuristic PII detection. (Redaction is explicit and config-driven only.)
- Default writes to anyone's PostHog instance — including ours — without explicit configuration.
- Default browser writes to anyone's PostHog instance — including ours — without explicit configuration.

## Reporting an issue

Expand Down
3 changes: 1 addition & 2 deletions libs/telemetry/ng-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@
"dest": "../../dist/libs/telemetry/browser",
"lib": {
"entryFile": "src/browser/public-api.ts"
},
"allowedNonPeerDependencies": ["posthog-js", "posthog-node"]
}
}
16 changes: 7 additions & 9 deletions libs/telemetry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
"bugs": { "url": "https://github.com/cacheplane/angular-agent-framework/issues" },
"sideEffects": false,
"type": "module",
"bin": {
"ngaf-telemetry-postinstall": "./node/postinstall.js"
},
"exports": {
".": {
"types": "./index.d.ts",
Expand All @@ -35,16 +38,11 @@
"./README.md": "./README.md"
},
"peerDependencies": {
"@angular/core": "^20.0.0 || ^21.0.0"
"@angular/core": "^20.0.0 || ^21.0.0",
"posthog-js": "^1.372.0"
},
"peerDependenciesMeta": {
"@angular/core": { "optional": true }
},
"dependencies": {
"posthog-js": "^1.372.0",
"posthog-node": "^5.20.0"
},
"scripts": {
"postinstall": "node ./node/postinstall.js || true"
"@angular/core": { "optional": true },
"posthog-js": { "optional": true }
}
}
Loading
Loading