Skip to content
Open
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
7 changes: 7 additions & 0 deletions apps/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@
"description": "Instrument an app's LLM calls so they emit $ai_generation events",
"ciCapable": true
},
{
"id": "metrics",
"dir": "metrics",
"label": "Metrics",
"description": "Instrument a service with posthog.metrics counters, gauges, and histograms",
"ciCapable": true
},
{
"id": "skill",
"dir": "misc",
Expand Down
26 changes: 26 additions & 0 deletions apps/metrics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Metrics test apps

Apps for testing `wizard metrics` (`posthog.metrics` counters, gauges,
histograms). Each app pins one starting state the verify step has to handle,
so together they cover install, upgrade, and reuse.

Layout: `<docs-installation-slug>/<runtime>-<app-name>`, matching the pages
under [installation docs](https://posthog.com/docs/metrics/installation).

## The apps

- `python/flask-jobqueue` — PostHog installed at a **pre-metrics version** and
initialized without metrics config, plus an existing capture call. Tests the
upgrade path: bump the SDK, add `metrics={"service_name": ...}` to the
existing client, leave the capture call alone.
- `nodejs/express-orders` — **no PostHog at all**. Tests the fresh path:
install `posthog-node`, initialize with metrics config, instrument the
request middleware, the background job, and the external call.
- `nextjs/nextjs-storefront` — **full-stack with `posthog-js` only**. Tests
variant disambiguation: metrics measure service work, so the right pick is
the server variant (fresh `posthog-node` for the route handlers), not
bolting metrics onto the browser client.

Every app has obvious choke points (request handling, a background job, an
external call, a business commit site) so instrumentation quality is
comparable across runs.
21 changes: 21 additions & 0 deletions apps/metrics/nextjs/nextjs-storefront/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# nextjs-storefront

Minimal full-stack Next.js shop: a client page with `posthog-js` initialized,
and a checkout route handler that calls a payment API server-side.

The disambiguation under test: metrics measure service work, so the right
variant is the **server** one — a fresh `posthog-node` client for the route
handler — not bolting metrics onto the existing browser client.

Run:

```bash
npm install
npm run dev
```

Traffic: open http://localhost:3000 and click "Buy a widget", or

```bash
curl -X POST localhost:3000/api/checkout -H 'Content-Type: application/json' -d '{"item": "widget", "qty": 1}'
```
23 changes: 23 additions & 0 deletions apps/metrics/nextjs/nextjs-storefront/app/api/checkout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextResponse } from 'next/server';

const PAYMENT_URL = 'https://httpbin.org/status/200';

let orderCount = 0;

export async function POST(req: Request) {
const payload = await req.json();
orderCount += 1;
try {
await fetch(PAYMENT_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: orderCount, ...payload }),
});
} catch {
return NextResponse.json(
{ id: orderCount, status: 'payment-unreachable' },
{ status: 502 },
);
}
return NextResponse.json({ id: orderCount, status: 'paid' });
}
17 changes: 17 additions & 0 deletions apps/metrics/nextjs/nextjs-storefront/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { PostHogProvider } from './providers';

export const metadata = { title: 'Storefront' };

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<PostHogProvider>{children}</PostHogProvider>
</body>
</html>
);
}
25 changes: 25 additions & 0 deletions apps/metrics/nextjs/nextjs-storefront/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client';

import { useState } from 'react';

export default function Home() {
const [status, setStatus] = useState('');

const checkout = async () => {
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ item: 'widget', qty: 1 }),
});
const body = await res.json();
setStatus(`order ${body.id}: ${body.status}`);
};

return (
<main>
<h1>Storefront</h1>
<button onClick={checkout}>Buy a widget</button>
<p>{status}</p>
</main>
);
}
14 changes: 14 additions & 0 deletions apps/metrics/nextjs/nextjs-storefront/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';

import posthog from 'posthog-js';
import { useEffect } from 'react';

export function PostHogProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY ?? 'phc_test_dummy_key', {
api_host:
process.env.NEXT_PUBLIC_POSTHOG_HOST ?? 'https://us.i.posthog.com',
});
}, []);
return <>{children}</>;
}
21 changes: 21 additions & 0 deletions apps/metrics/nextjs/nextjs-storefront/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "nextjs-storefront",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^15.1.0",
"posthog-js": "^1.200.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "20.17.6",
"@types/react": "19.2.18",
"typescript": "5.8.2"
}
}
20 changes: 20 additions & 0 deletions apps/metrics/nextjs/nextjs-storefront/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
21 changes: 21 additions & 0 deletions apps/metrics/nodejs/express-orders/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# express-orders

Tiny order API with a background fulfillment loop. No PostHog anywhere — the
fresh path under test: install `posthog-node`, initialize one client with
`metrics: { serviceName }`, instrument the request handling, the background
job, and the external call.

Run:

```bash
npm install
npm start
```

Traffic:

```bash
curl -X POST localhost:5002/orders -H 'Content-Type: application/json' -d '{"item": "widget", "qty": 2}'
curl localhost:5002/orders
curl localhost:5002/health
```
12 changes: 12 additions & 0 deletions apps/metrics/nodejs/express-orders/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "express-orders",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node src/server.js"
},
"dependencies": {
"express": "^4.19.0"
}
}
27 changes: 27 additions & 0 deletions apps/metrics/nodejs/express-orders/src/fulfillment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Fulfillment queue: pending orders drain to an external API in the background.
const FULFILLMENT_URL = 'https://httpbin.org/status/200';

const pending = [];

export function submitOrder(order) {
pending.push(order);
}

export function pendingCount() {
return pending.length;
}

export async function fulfillPending() {
while (pending.length > 0) {
const order = pending.shift();
try {
await fetch(FULFILLMENT_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(order),
});
} catch {
// Fulfillment API unreachable: drop the order for this toy app.
}
}
}
38 changes: 38 additions & 0 deletions apps/metrics/nodejs/express-orders/src/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Order API: accepts orders over HTTP, retries fulfillment in a background loop.
import express from 'express';

import { fulfillPending, pendingCount, submitOrder } from './fulfillment.js';

const app = express();
app.use(express.json());

const orders = [];

app.post('/orders', (req, res) => {
const order = {
id: orders.length + 1,
item: req.body.item ?? 'unknown',
qty: Number(req.body.qty ?? 1),
createdAt: Date.now(),
};
orders.push(order);
submitOrder(order);
res.status(201).json(order);
});

app.get('/orders', (_req, res) => {
res.json(orders);
});

app.get('/health', (_req, res) => {
res.json({ ok: true, pending: pendingCount() });
});

// Background job: drain pending fulfillments every two seconds.
setInterval(() => {
fulfillPending().catch(() => {});
}, 2000);

app.listen(5002, () => {
console.log('express-orders listening on :5002');
});
23 changes: 23 additions & 0 deletions apps/metrics/python/flask-jobqueue/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# flask-jobqueue

Tiny order-processing service: HTTP API in, background worker out.

PostHog is already here — `posthog==3.8.3` (pre-metrics) with a client
initialized in `app.py` and a capture call on order creation. The upgrade
path under test: bump the SDK past the metrics floor, add
`metrics={"service_name": ...}` to the existing client, touch nothing else.

Run:

```bash
pip install -r requirements.txt
python app.py
```

Traffic:

```bash
curl -X POST localhost:5001/orders -H 'Content-Type: application/json' -d '{"item": "widget", "qty": 2}'
curl localhost:5001/orders
curl localhost:5001/health
```
54 changes: 54 additions & 0 deletions apps/metrics/python/flask-jobqueue/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Order API: accepts orders over HTTP, hands them to the background worker."""

import os
import time

from flask import Flask, jsonify, request
from posthog import Posthog

from worker import JobQueue

app = Flask(__name__)

posthog = Posthog(
os.environ.get("POSTHOG_API_KEY", "phc_test_dummy_key"),
host=os.environ.get("POSTHOG_HOST", "https://us.i.posthog.com"),
)

queue = JobQueue()

ORDERS: list[dict] = []


@app.post("/orders")
def create_order():
payload = request.get_json(force=True)
order = {
"id": len(ORDERS) + 1,
"item": payload.get("item", "unknown"),
"qty": int(payload.get("qty", 1)),
"created_at": time.time(),
}
ORDERS.append(order)
queue.submit(order)
posthog.capture(
distinct_id=f"user_{order['id'] % 7}",
event="order created",
properties={"item": order["item"], "qty": order["qty"]},
)
return jsonify(order), 201


@app.get("/orders")
def list_orders():
return jsonify(ORDERS)


@app.get("/health")
def health():
return jsonify({"ok": True, "queued": queue.depth()})


if __name__ == "__main__":
queue.start()
app.run(port=5001, debug=False)
3 changes: 3 additions & 0 deletions apps/metrics/python/flask-jobqueue/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
flask>=3.0.0
posthog==3.8.3
requests>=2.31.0
Loading