Option 2
Set up with a JS app
Import a tiny helper into a React / Next / Vue / Svelte app. It reads device_id and session_id from the Amplitude browser SDK, hits /api/chat, and yields streamed message deltas you can render into state.
Install dependencies
npm install @amplitude/analytics-browser @amplitude/plugin-session-replay-browser
Initialize the SDK once, at app startup
In a Next.js app this goes in a client component mounted at the root (e.g. app/providers.tsx). For a React/Vite app, in main.tsx.
import * as amplitude from "@amplitude/analytics-browser";
import { sessionReplayPlugin } from "@amplitude/plugin-session-replay-browser";
amplitude.add(sessionReplayPlugin({ sampleRate: 1 }));
amplitude.init(process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY!, {
autocapture: true,
defaultTracking: true,
});Add the streaming helper
One async generator. Yields assistant deltas as they arrive, plus an AbortSignal hook so the caller can cancel an in-flight stream.
import * as amplitude from "@amplitude/analytics-browser";
export type ChatMessage = { role: "user" | "assistant" | "system"; content: string };
export async function* streamAgent(opts: {
endpoint: string; // e.g. "https://demo-agent-analytics-server.vercel.app/api/chat"
messages: ChatMessage[];
agentId: string;
apiKey: string; // Amplitude project API key
systemPrompt?: string;
signal?: AbortSignal;
}): AsyncGenerator<string, void> {
const res = await fetch(opts.endpoint, {
method: "POST",
headers: { "content-type": "application/json" },
signal: opts.signal,
body: JSON.stringify({
messages: opts.messages,
agentId: opts.agentId,
systemPrompt: opts.systemPrompt,
amplitude: {
apiKey: opts.apiKey,
deviceId: amplitude.getDeviceId(),
sessionId: amplitude.getSessionId(),
userId: amplitude.getUserId(),
},
}),
});
if (!res.ok || !res.body) {
throw new Error(`agent stream failed: ${res.status}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
yield decoder.decode(value, { stream: true });
}
}Use it from a React component
The streaming helper is framework-agnostic — here's a minimal React example. The same pattern works in Vue / Svelte / Solid by replacing useState with the local reactive primitive.
"use client";
import { useRef, useState } from "react";
import { streamAgent, type ChatMessage } from "@/lib/streamAgent";
export function ShoppingChat() {
const [history, setHistory] = useState<ChatMessage[]>([]);
const [input, setInput] = useState("");
const [busy, setBusy] = useState(false);
const abortRef = useRef<AbortController | null>(null);
async function send(e: React.FormEvent) {
e.preventDefault();
if (!input.trim() || busy) return;
const next: ChatMessage[] = [...history, { role: "user", content: input }];
setHistory([...next, { role: "assistant", content: "" }]);
setInput("");
setBusy(true);
abortRef.current = new AbortController();
let full = "";
try {
for await (const delta of streamAgent({
endpoint: "https://demo-agent-analytics-server.vercel.app/api/chat",
messages: next,
agentId: "shopping-agent",
apiKey: process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY!,
signal: abortRef.current.signal,
})) {
full += delta;
setHistory((h) => {
const copy = h.slice();
copy[copy.length - 1] = { role: "assistant", content: full };
return copy;
});
}
} finally {
setBusy(false);
}
}
return (
<form onSubmit={send}>
{/* render history + input here */}
</form>
);
}Verify the wiring
Open User Lookup in the Amplitude project for the API key you used. You should see, on the same Session Replay timeline:
[Agent] Message Sent[Agent] Message Receivedwithprompt_tokens,completion_tokens, latency.
If they're missing, double-check that amplitude.getSessionId() returns a number (not a string) and that the Amplitude project zone matches serverZone (default "US").