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.

1

Install dependencies

bash
npm install @amplitude/analytics-browser @amplitude/plugin-session-replay-browser
2

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.

ts
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,
});
3

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.

ts
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 });
  }
}
4

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.

tsx
"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>
  );
}
5

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 Received with prompt_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").