Skip to main content
Solution accelerator
Customer-facing AI agents

Build a real-time context-aware agent with Signals, Google ADK, and CopilotKit

Add Snowplow Signals to a Google ADK agent so it gets real-time user context for every response, and embed it in a React app with CopilotKit.

Progress0%

Set up Snowplow tracking

Snowplow Signals computes user attributes from your Snowplow behavioral event stream. Follow these steps to create events to work with.

Signals doesn't need custom events: standard page views, page pings, and link clicks are enough to build meaningful real-time context.

Since you're building a Next.js app, you'll use the Snowplow Browser tracker, which is designed for npm-based frameworks.

Initialize the tracker​

Create a module that initializes the tracker once and exports a helper to read the session ID. This module runs client-side only.

tsx
// src/lib/snowplow.ts
import {
newTracker,
trackPageView,
enableActivityTracking,
type BrowserTracker,
} from "@snowplow/browser-tracker";
import {
LinkClickTrackingPlugin,
enableLinkClickTracking,
} from "@snowplow/browser-plugin-link-click-tracking";

let tracker: BrowserTracker | null = null;

export function initSnowplow() {
if (tracker || typeof window === "undefined") return;

const collectorUrl = process.env.NEXT_PUBLIC_SNOWPLOW_COLLECTOR_URL;
if (!collectorUrl) return;

tracker =
newTracker("sp", collectorUrl, {
appId: "signals-adk-agent",
plugins: [LinkClickTrackingPlugin()],
}) ?? null;

enableActivityTracking({
minimumVisitLength: 30,
heartbeatDelay: 10,
});

enableLinkClickTracking({ pseudoClicks: true });
}

export function trackPage() {
trackPageView();
}

export function getDomainSessionId(): string {
if (!tracker) return "";
try {
// getDomainUserInfo() returns the _sp_id cookie as an array.
// Index [6] is the domain_sessionid.
const info = tracker.getDomainUserInfo();
return info?.[6] ?? "";
} catch {
return "";
}
}

The session ID is stored in the _sp_id cookie, which the getDomainUserInfo() method reads. You'll use this value later to fetch the current user's Signals attributes.

Track page views on route changes​

In a Next.js App Router app, client-side navigation doesn't trigger full page reloads.

Create a client component to track page views when the route changes:

tsx
// src/components/snowplow-provider.tsx
"use client";

import { useEffect } from "react";
import { usePathname } from "next/navigation";
import { initSnowplow, trackPage } from "@/lib/snowplow";

export function SnowplowProvider({ children }: { children: React.ReactNode }) {
const pathname = usePathname();

useEffect(() => {
initSnowplow();
}, []);

useEffect(() => {
trackPage();
}, [pathname]);

return <>{children}</>;
}
Using a different framework?

The scaffold uses usePathname from next/navigation for route-change detection. In a Vite application, swap this for useLocation from react-router-dom. The Snowplow tracker itself is framework-agnostic.

Add to the root layout​

Wrap your app with the SnowplowProvider outside the CopilotKit provider. The Snowplow tracker must initialize first so its cookie exists before CopilotProvider tries to read the session ID.

tsx
// src/app/layout.tsx — modify the scaffolded root component:
import type { Metadata } from "next";
import { SnowplowProvider } from "@/components/snowplow-provider";
import { CopilotProvider } from "@/components/copilot-provider";
import { ChatShell } from "@/components/chat-shell";
import "./globals.css";
import "@copilotkit/react-ui/styles.css";

export const metadata: Metadata = {
title: "Signals ADK Agent",
description: "Real-time user context for AI agents with Snowplow Signals",
};

export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body className={"antialiased"}>
<SnowplowProvider>
<CopilotProvider>
<ChatShell>{children}</ChatShell>
</CopilotProvider>
</SnowplowProvider>
</body>
</html>
);
}

CopilotProvider is a thin wrapper around <CopilotKit>. Create the placeholder version now so the layout compiles. You'll extend it in the next step to read the Snowplow session ID from the tracker cookie.

tsx
// src/components/copilot-provider.tsx
"use client";

import { CopilotKit } from "@copilotkit/react-core";

export function CopilotProvider({ children }: { children: React.ReactNode }) {
return (
<CopilotKit runtimeUrl="/api/copilotkit" agent="my_agent">
{children}
</CopilotKit>
);
}

ChatShell mounts the CopilotKit sidebar at the layout level so the chat persists across route changes.

tsx
// src/components/chat-shell.tsx
"use client";

import { CopilotSidebar } from "@copilotkit/react-ui";

export function ChatShell({ children }: { children: React.ReactNode }) {
return (
<CopilotSidebar
clickOutsideToClose={false}
defaultOpen={true}
labels={{
title: "Signal Shop Assistant",
initial:
"Hi! I'm your Signal Shop assistant. Browse around the store and I'll personalize my help based on what you're interested in.",
}}
>
{children}
</CopilotSidebar>
);
}

With this in place, every route change fires a page view event, page pings track ongoing engagement, and link clicks are captured automatically. That gives Signals a rich behavioral event stream to compute attributes from.

On this page

Want to see a custom demo?

Our technical experts are here to help.