Skip to main content

Overview

Cloudflare Think uses the Vercel AI SDK internally. Think owns the streamText call, so add Latitude telemetry in beforeTurn() instead of at a model call site. Start with the basic setup. Add context only if you need traces to include user, session, tags, or metadata from your app.

Requirements

  • A Latitude API key
  • A Latitude project slug
  • A Cloudflare Workers project using @cloudflare/think
  • The nodejs_compat compatibility flag enabled in your Worker
  • LATITUDE_API_KEY and LATITUDE_PROJECT_SLUG configured as Worker secrets or variables

Basic Telemetry

This records Think model calls, tools, latency, tokens, prompts, and responses.
1

Install

npm install @latitude-data/telemetry
2

Initialize Latitude once

import { Think, type TurnConfig } from "@cloudflare/think"
import { Latitude } from "@latitude-data/telemetry"
import { routeAgentRequest } from "agents"
import { createWorkersAI } from "workers-ai-provider"

type Env = {
  AI: Ai
  LATITUDE_API_KEY: string
  LATITUDE_PROJECT_SLUG: string
}

const latitude = new Latitude({
  apiKey: process.env.LATITUDE_API_KEY!,
  project: process.env.LATITUDE_PROJECT_SLUG!,
  serviceName: "cloudflare-think-agent",
})

export class MyAgent extends Think<Env> {
  getModel() {
    return createWorkersAI({ binding: this.env.AI })("@cf/meta/llama-4-scout-17b-16e-instruct")
  }

  beforeTurn(): TurnConfig {
    return {
      experimental_telemetry: {
        isEnabled: true,
        tracer: latitude.getTracer("cloudflare-think"),
        functionId: "think-turn",
        metadata: { framework: "cloudflare-think" },
      },
    }
  }

  async onChatResponse() {
    await latitude.flush()
  }

  onChatError(error: unknown) {
    this.ctx.waitUntil(latitude.flush())
    return error
  }
}

export default {
  async fetch(request: Request, env: Env) {
    const response =
      (await routeAgentRequest(request, env)) ??
      new Response("Not found", { status: 404 })

    return response
  },
} satisfies ExportedHandler<Env>
If your Worker exposes secrets through process.env, this is all you need. Most Workers receive secrets through env bindings instead. In that case, keep one Latitude instance per Worker isolate:
let latitude: Latitude | undefined

function getLatitude(env: Env) {
  latitude ??= new Latitude({
    apiKey: env.LATITUDE_API_KEY,
    project: env.LATITUDE_PROJECT_SLUG,
    serviceName: "cloudflare-think-agent",
  })

  return latitude
}
Then use getLatitude(this.env).getTracer("cloudflare-think") in beforeTurn() and getLatitude(this.env).flush() in onChatResponse() / onChatError(). Do not create new Latitude() inside beforeTurn().

Add App Context

If your app uses Think’s default WebSocket chat entrypoint, pass the same context you would normally pass to capture() through useAgentChat({ body }).
import { useAgentChat } from "@cloudflare/think/react"

const { messages, sendMessage } = useAgentChat({
  agent,
  body: {
    userId: currentUser.id,
    sessionId: session.id,
    tags: ["cloudflare-think"],
    metadata: { plan: currentUser.plan },
  },
})
Then read that context in beforeTurn() and pass it to getTracer():
import { Think, type TurnConfig, type TurnContext } from "@cloudflare/think"
import type { ContextOptions } from "@latitude-data/telemetry"

export class MyAgent extends Think<Env> {
  beforeTurn(ctx: TurnContext): TurnConfig {
    const context = (ctx.body ?? {}) as ContextOptions

    return {
      experimental_telemetry: {
        isEnabled: true,
        tracer: latitude.getTracer("cloudflare-think", context),
        functionId: "think-turn",
      },
    }
  }
}
This is the recommended setup for most Think apps.

Programmatic Turns

If your app starts Think turns with your own runTurn() call, wrap that call with capture().
import { capture } from "@latitude-data/telemetry"

export class MyAgent extends Think<Env> {
  async runSupportTurn(input: string, userId: string, sessionId: string) {
    return capture(
      "cloudflare-think-turn",
      () => this.runTurn({ input }),
      {
        userId,
        sessionId,
        tags: ["cloudflare-think"],
        metadata: { framework: "cloudflare-think" },
      },
    )
  }
}
Keep getTracer() in beforeTurn():
beforeTurn(): TurnConfig {
  return {
    experimental_telemetry: {
      isEnabled: true,
      tracer: latitude.getTracer("cloudflare-think"),
      functionId: "think-turn",
    },
  }
}
Do not use capture.start() / scope.end() on Cloudflare Workers. Use capture() as a callback wrapper instead.

If You Cannot Wrap The Turn

If you cannot wrap the turn, pass context directly to the tracer from beforeTurn(). The context can come from agent state, Durable Object storage, auth state, or any place your agent can read during the turn.
async beforeTurn(): Promise<TurnConfig> {
  const userId = await this.ctx.storage.get<string>("userId")
  const sessionId = await this.ctx.storage.get<string>("sessionId")

  return {
    experimental_telemetry: {
      isEnabled: true,
      tracer: latitude.getTracer("cloudflare-think", {
        userId,
        sessionId,
        tags: ["cloudflare-think"],
      }),
      functionId: "think-turn",
    },
  }
}
This records the model and tool spans with the right context. It does not add a separate parent cloudflare-think-turn span.

Runnable Example

The Latitude repository includes a runnable Think example at examples/cloudflare-think-app. It includes a getWeather tool, a small QA page, and a local verifier that checks model spans, tool spans, userId, and sessionId against local Latitude.

Seeing Your Traces

Once connected, traces appear automatically in Latitude:
  1. Open your project in the Latitude dashboard
  2. Send a message to your Think agent
  3. The turn appears with model calls, tool calls, messages, latency, token usage, and errors