npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

lumina-sdk

v2.0.9

Published

Grounded Intelligence SDK: Connect your AI workflows to every surrounding data stream — session replays, user feedback, observability traces, and more.

Readme

Grounded Intelligence SDK

Connect your AI workflows to every surrounding data stream — session replays, user feedback, observability traces, and more.

npm version License: MIT TypeScript


📋 Table of Contents


The Problem

Building is no longer the bottleneck — understanding what to build is.

Product teams are building blindly, asking critical questions that traditional analytics can't answer:

  • What actually moves the needle on user retention?
  • What turns first-time users into power users?
  • Where does my AI product produce the "AHA" moment, and when does it create friction?

These answers live in every user interaction pattern, but extracting value is fractured and manual for two key reasons:

1. User data lives in silos across your entire stack

  • Explicit feedback (NPS surveys, thumbs up/down, customer support) captures a small fraction of users who voluntarily articulate what frustrated them
  • Implicit behavior (session replays, click patterns) shows what led users to abandon
  • System traces show when services failed during their session

Each data stream lives in disconnected tools — the complete user story emerges only when you integrate and analyze them together.

2. AI is the central workflow but analytics hasn't adapted

Users now increasingly interact with products through AI assistants and agents — taking actions, completing tasks, and answering questions through natural language. Current analytics tooling is built for traditional click-based workflows, not AI-centric ones.

Evaluating the success and relevance of AI systems requires connecting them to surrounding context: what the user did before the conversation, what happened during generation, and what happened after.


The Solution

Grounded Intelligence connects your AI workflows to every surrounding data stream and actively watches them — session replays (available now), customer support conversations (coming soon), observability traces (coming soon), payment events (coming soon), and user feedback (coming soon).

The SDK puts AI at the center while seamlessly integrating with your existing analytics providers. Track every LLM conversation alongside the complete user journey.

What This SDK Enables

  • Capture AI Conversations: Track every user ↔ assistant interaction with automatic latency, error capture, and context
  • Connect to Session Replays: Correlate AI conversations with session replays (PostHog integration available now, more providers coming)
  • Track Tool & API Calls: Monitor external API calls, database queries, and function executions made by your AI
  • Unified User Identity: Seamlessly transition from anonymous to identified users across all data streams
  • Privacy-First: Built-in transcript masking and PII filtering

Key Features

  • Minimal Overhead: Buffered NDJSON transport with automatic batching
  • Session Management: W3C Trace Context standard for distributed tracing
  • Persistent Identity: Cookie + localStorage for identity across sessions and tabs
  • Provider Agnostic: Pluggable integration with UI analytics (PostHog now, more coming) and system observability
  • Type Safe: Full TypeScript support with exported types

What's Coming

Grounded Intelligence is actively building integrations for:

  • Customer support platforms (Intercom, Zendesk)
  • Observability providers (DataDog, Sentry, OpenTelemetry)
  • Payment & business events (Stripe, custom webhooks)
  • User feedback systems (in-app surveys, feature requests)

Installation

npm install luminasdk

Note: The package is installed as luminasdk, but throughout the codebase and documentation, we refer to it as the Grounded Intelligence SDK.

Optional Peer Dependencies

For UI provider integration:

npm install posthog-js  # For PostHog integration

Quick Start

1. Initialize the SDK

import { Lumina, CaptureTranscript } from 'luminasdk'

Lumina.init({
  endpoint: 'https://your-ingest-server.com',
  writeKey: 'gi_xxxxx',  // API key from your Grounded Intelligence dashboard
  captureTranscript: CaptureTranscript.Full,
  maskFn: (text) => text,
  enableToolWrapping: true,
  flushIntervalMs: 5000,
  maxBatchBytes: 100_000,
  uiAnalytics: createDummyProvider(),  // See Provider Integration below
})

2. Start a Session and Capture a Turn

// Start a session (once per conversation)
const session = await Lumina.session.start()

// Create a turn (once per user message → assistant response)
const turn = session.turn()

// Wrap your LLM call
const response = await turn.wrapLLM(
  async () => {
    // Your OpenAI/Anthropic/etc call here
    return await openai.chat.completions.create({
      model: 'gpt-4o',
      messages: [{ role: 'user', content: 'Hello!' }],
    })
  },
  { model: 'gpt-4o', prompt_id: 'greeting_v1' }
)

// Provide transcript (respects captureTranscript setting)
turn.setMessages([
  { role: 'user', content: 'Hello!' },
  { role: 'assistant', content: response.choices[0].message.content || '' },
])

// Finish turn (sends event to ingest)
await turn.finish()

Core Concepts

Turn Event

A turn represents a single user ↔ assistant interaction. Each turn captures:

  • Latency: Time from LLM call start to completion
  • Model & Prompt: Which model and prompt version was used
  • Transcript: User and assistant messages (with privacy controls)
  • Tool Calls: External API/database calls made during the turn
  • Retrieval: Context retrieved for RAG applications
  • Annotations: Custom metadata (UI session IDs, replay URLs, etc.)

Session

A session is a sequence of turns under a single trace ID. Sessions:

  • Use W3C Trace Context standard (traceparent header format)
  • Persist across page reloads via sessionStorage
  • Reset on user identity changes (optional)

Identity

User identity can be:

  • Anonymous: Auto-generated anon_<uuid> on first use
  • Identified: Explicitly set via Lumina.identify(userId, properties)

Identity is persisted in:

  1. Cookies: Primary storage (lumina_id, 1-year expiry)
  2. localStorage: Backup storage

API Reference

Initialization

Lumina.init(config: LuminaConfig)

Initialize the SDK once at app startup.

Config:

{
  writeKey: string              // API key from dashboard (gi_...)
  endpoint: string              // Ingest server URL
  captureTranscript: CaptureTranscript  // Full | Masked | None
  maskFn: (text: string) => string      // PII masking function
  enableToolWrapping: boolean   // Enable automatic tool call tracking
  flushIntervalMs: number       // Buffer flush interval (default: 5000ms)
  maxBatchBytes: number         // Max batch size before auto-flush (default: 100KB)
  uiAnalytics: UIAnalyticsProvider      // UI provider (PostHog, etc.)
  systemAnalytics?: SystemAnalyticsProvider  // Optional: system tracing provider
}

CaptureTranscript Options:

  • CaptureTranscript.Full: Capture all messages verbatim
  • CaptureTranscript.Masked: Apply maskFn before sending
  • CaptureTranscript.None: Don't capture transcript content

Session Management

await Lumina.session.start() → SessionCtx

Start a new session. Returns a session context with a turn() method.

Returns:

{
  turn: () => TurnCtx
}

Note: Multiple session.start() calls will reuse the same trace ID unless the session is explicitly cleared.


Turn Management

session.turn() → TurnCtx

Create a new turn context. Each turn gets a unique span ID and incremented sequence number.

Turn Context Methods:

await turn.wrapLLM<T>(fn: () => Promise<T>, meta: { model: string; prompt_id: string }) → Promise<T>

Wrap an LLM call to capture latency, model, and errors.

Example:

const response = await turn.wrapLLM(
  async () => await openai.chat.completions.create({ /* ... */ }),
  { model: 'gpt-4o', prompt_id: 'chat_v1' }
)

Captures:

  • Latency (milliseconds)
  • Model name
  • Prompt ID (for versioning)
  • Error state (if thrown)

Does NOT capture:

  • Token usage (must be added via annotate())
  • Streaming progress

await turn.recordTool<T>(name: string, fn: () => Promise<T>, opts: { type: string; target: string; version: string }) → Promise<T>

Wrap a tool call (API, database, function) to capture execution metrics.

Example:

const results = await turn.recordTool(
  'search_docs',
  async () => await vectorDB.search(query),
  { type: 'vector_search', target: 'pinecone', version: 'v1' }
)

Captures:

  • Success/failure
  • Latency
  • Error message (if thrown)

Note: Only active if enableToolWrapping: true in config.


turn.addRetrieval(info: RetrievalInfo)

Attach retrieval metadata for RAG applications.

Example:

turn.addRetrieval({
  source: 'docs',
  query: 'How do I reset my password?',
  results: [
    { id: 'doc_123', score: 0.95, content: '...' },
    { id: 'doc_456', score: 0.87 },
  ],
})

turn.annotate(obj: Record<string, any>)

Attach custom metadata to the turn.

Example:

turn.annotate({
  ui_session_id: posthog.get_session_id(),
  ui_replay_url: 'https://app.posthog.com/...',
  custom_tag: 'high_priority',
})

Common Use Cases:

  • UI session IDs and replay URLs
  • Feature flags
  • A/B test variants
  • Custom business logic metadata

turn.setMessages(msgs: Array<{ role: string; content: string }>)

Set the conversation transcript for this turn.

Example:

turn.setMessages([
  { role: 'user', content: 'What is the weather?' },
  { role: 'assistant', content: 'The weather is sunny.' },
])

Privacy:

  • CaptureTranscript.Full: Sent as-is
  • CaptureTranscript.Masked: Content passed through maskFn before sending
  • CaptureTranscript.None: Content not sent (only metadata)

await turn.finish()

Finalize the turn and flush to ingest server.

Important:

  • Always call finish() to ensure events are sent
  • Use try/finally blocks to guarantee execution
  • Calling finish() multiple times is safe (idempotent after first call)

Example:

const turn = session.turn()
try {
  // ... LLM call, tool calls, etc.
  await turn.finish()
} catch (error) {
  // Error handling
}

User Identity

await Lumina.identify(userId: string, properties?: Record<string, any>, opts?: { newSession?: boolean })

Identify the current user. Switches from anonymous to known identity.

Parameters:

  • userId: Unique user identifier (e.g., database ID, email)
  • properties: Optional user properties (name, email, etc.)
  • opts.newSession: If true, clears current session and starts fresh

Example:

// After user login
await Lumina.identify('user_123', {
  email: '[email protected]',
  name: 'John Doe',
})

Behavior:

  • Persists userId to cookie and localStorage
  • Updates distinctId on all subsequent events
  • Sends identify event to backend for user aliasing
  • Syncs identity to UI provider (PostHog, Amplitude, etc.) if supported
  • If user changes, clears session by default
  • Flushes buffer immediately to ensure alias is created

Lumina.reset()

Reset to anonymous identity. Typically called on logout.

Behavior:

  • Generates new anon_<uuid> ID
  • Clears session trace ID
  • Persists new anonymous ID to cookie and localStorage

Example:

// On user logout
Lumina.reset()

Lumina.getDistinctId() → string

Get the current user's distinct ID (anonymous or identified).

Example:

const userId = Lumina.getDistinctId()
console.log(userId)  // 'anon_abc123' or 'user_123'

Lumina.isAnonymous() → boolean

Check if the current user is anonymous.

Example:

if (Lumina.isAnonymous()) {
  console.log('User not logged in')
}

User Identity Management

Identity Lifecycle

  1. First Visit: Auto-generates anon_<uuid>
  2. Login: Call identify(userId) to switch to known user
  3. Logout: Call reset() to return to anonymous
  4. Return Visit: Loads identity from cookie/localStorage

Storage Mechanism

Primary: Cookies

  • Key: lumina_id
  • Expiry: 1 year
  • SameSite: Lax
  • Path: /

Backup: localStorage

  • Key: lumina_id
  • No expiry (unless manually cleared)

Session Continuity

Sessions persist across page reloads via sessionStorage (traceparent key). Sessions are cleared when:

  • User identity changes
  • Lumina.reset() is called
  • sessionStorage is manually cleared

Provider Integration

PostHog Integration

Connect your AI conversations with session replays to see the complete user journey — what they clicked, where they navigated, and what they did before and after each AI interaction.

Coming Soon: Integrations for Amplitude, FullStory, LogRocket, and custom replay providers.

Setup

import posthog from 'posthog-js'
import { postHogProvider } from 'luminasdk/providers/posthog'

// Initialize PostHog
posthog.init('phc_xxxxx', {
  api_host: 'https://us.posthog.com',
  session_recording: { recordCrossOriginIframes: true },
})

// Create provider adapter
const uiProvider = postHogProvider({
  apiKey: 'phc_xxxxx',
  host: 'https://us.posthog.com',
  projectId: '12345',  // Optional: enables replay URL generation
})

// Initialize SDK with UI provider
Lumina.init({
  // ... other config
  uiAnalytics: uiProvider,
})

Usage

const session = await Lumina.session.start()
const turn = session.turn()

// Attach UI pointers via annotations
turn.annotate({
  ui_session_id: uiProvider.getSessionId(),
  ui_replay_url: uiProvider.getReplayUrl(),
})

await turn.finish()

How It Works

  • Super Properties: SDK tags PostHog events with lumina_session_id, lumina_turn_id, lumina_turn_seq, and lumina_turn_start_ts
  • Tag/Untag: Properties are registered on turn start and unregistered on finish
  • Correlation: Enables joining AI conversation events with session replay events in the Grounded Intelligence dashboard

This creates a unified timeline where you can:

  • Jump from an AI conversation directly to the session replay
  • See what UI actions preceded or followed each AI interaction
  • Understand the full context of user behavior patterns

Dummy Provider (No UI Analytics)

If you don't use a UI analytics provider, use a dummy implementation:

function createDummyProvider() {
  return {
    name: 'none',
    init: () => {},
    getSessionId: () => '',
    getReplayUrl: () => '',
    tagTurn: () => {},
    untagTurn: () => {},
    startReplay: () => {},
    stopReplay: () => {},
    captureEvent: () => {},
    shutdown: () => {},
  }
}

Lumina.init({
  // ... other config
  uiAnalytics: createDummyProvider(),
})

Advanced Features

Chat Input Tracking

Track implicit user behavior signals like typing duration, hesitation, and abandonment using ChatInputTracker. These signals reveal user friction points that explicit feedback misses.

Setup:

import { ChatInputTracker } from 'luminasdk'

const session = await Lumina.session.start()
const tracker = new ChatInputTracker(uiProvider, sessionId)

Usage:

// When user focuses on input
inputElement.addEventListener('focus', () => {
  tracker.onInputStart()
})

// When user submits message
form.addEventListener('submit', () => {
  tracker.onMessageSubmit(messageId, inputValue)
})

// When user abandons input (blurs with content)
inputElement.addEventListener('blur', () => {
  if (inputElement.value.trim()) {
    tracker.onInputAbandoned(inputElement.value)
  }
})

Captured Events:

  • chat_input_started: When user begins typing
  • chat_message_submitted: When message is sent (includes typing duration)
  • chat_input_abandoned: When user types but doesn't send (partial content is hashed to avoid PII)

Key Metrics:

  • typing_duration_ms: Time spent composing the message
  • character_count: Length of input
  • partial_content_hash: Hash of abandoned content (no PII stored)

PII Masking

Mask sensitive data before sending to ingest server.

Lumina.init({
  captureTranscript: CaptureTranscript.Masked,
  maskFn: (text) => {
    // Mask emails
    text = text.replace(/\b[\w.-]+@[\w.-]+\.\w{2,}\b/g, '[EMAIL]')
    // Mask phone numbers
    text = text.replace(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, '[PHONE]')
    // Mask credit cards
    text = text.replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, '[CARD]')
    return text
  },
})

Buffered Transport

Events are buffered and sent in batches to minimize network overhead.

Auto-Flush Triggers:

  1. Timer: Every flushIntervalMs (default: 5000ms)
  2. Batch Size: When maxBatchBytes is exceeded (default: 100KB)
  3. Visibility Change: On tab hide or page unload

Retry Logic:

  • 3 retry attempts with linear backoff (300ms, 600ms, 900ms)
  • Failed batches are re-queued

Custom System Analytics Provider

Integrate with distributed tracing systems (OpenTelemetry, Datadog, etc.).

const customSystemProvider: SystemAnalyticsProvider = {
  name: 'opentelemetry',
  startTurn: (ctx) => {
    const span = tracer.startSpan('turn', { attributes: ctx })
    return { span_id: span.spanContext().spanId }
  },
  endTurn: (spanId, status, meta) => {
    const span = getSpan(spanId)
    span.setStatus({ code: status === 'ok' ? SpanStatusCode.OK : SpanStatusCode.ERROR })
    span.end()
  },
  startSpan: (ctx) => { /* ... */ },
  endSpan: (spanId, status, meta) => { /* ... */ },
  shutdown: () => tracer.shutdown(),
}

Lumina.init({
  // ... other config
  systemAnalytics: customSystemProvider,
})

TypeScript Support

All public types and classes are exported:

import type {
  LuminaConfig,
  SessionCtx,
  TurnCtx,
  ToolCall,
  RetrievalInfo,
  UIAnalyticsProvider,
  SystemAnalyticsProvider,
  CaptureTranscript,
} from 'luminasdk'

// Classes
import { ChatInputTracker } from 'luminasdk'

Type Definitions

LuminaConfig: SDK initialization config
SessionCtx: Session context returned by session.start()
TurnCtx: Turn context returned by session.turn()
ToolCall: Tool call metadata
RetrievalInfo: RAG retrieval metadata
UIAnalyticsProvider: UI provider adapter interface
SystemAnalyticsProvider: System tracing provider interface
CaptureTranscript: Transcript capture mode enum
ChatInputTracker: Class for tracking chat input behavior and abandonment


Best Practices

1. Initialize Once

// ✅ Good: Initialize at app startup
Lumina.init({ /* ... */ })

// ❌ Bad: Initialize on every request
function handleRequest() {
  Lumina.init({ /* ... */ })  // Don't do this
}

2. Use One Session Per Conversation

// ✅ Good: One session per chat thread
const session = await Lumina.session.start()

// Multiple turns in the same session
for (const userMessage of messages) {
  const turn = session.turn()
  // ... handle turn
  await turn.finish()
}

3. Always Call finish()

// ✅ Good: Use try/finally to guarantee finish
const turn = session.turn()
try {
  await turn.wrapLLM(/* ... */)
  turn.setMessages(/* ... */)
} finally {
  await turn.finish()
}

// ❌ Bad: finish() might not be called on error
const turn = session.turn()
await turn.wrapLLM(/* ... */)
await turn.finish()  // Skipped if wrapLLM throws

4. Identify Users Early

// ✅ Good: Identify immediately after login
async function login(email, password) {
  const user = await authenticate(email, password)
  await Lumina.identify(user.id, { email: user.email, name: user.name })
}

5. Mask PII

// ✅ Good: Use Masked mode with custom maskFn
Lumina.init({
  captureTranscript: CaptureTranscript.Masked,
  maskFn: (text) => {
    // Custom PII masking logic
    return text.replace(/\b[\w.-]+@[\w.-]+\.\w{2,}\b/g, '[EMAIL]')
  },
})

Advanced Use Cases

Streaming LLM Responses

Tracking streaming completions:

import { OpenAI } from 'openai'

const turn = session.turn()
const startTime = Date.now()

try {
  const stream = await turn.wrapLLM(
    async () => {
      return await openai.chat.completions.create({
        model: 'gpt-4o',
        messages: [{ role: 'user', content: userMessage }],
        stream: true,
      })
    },
    { model: 'gpt-4o', prompt_id: 'chat_v1' }
  )

  let fullResponse = ''
  for await (const chunk of stream) {
    const content = chunk.choices[0]?.delta?.content || ''
    fullResponse += content
    // Display to user in real-time
    displayChunk(content)
  }

  // Record complete transcript
  turn.setMessages([
    { role: 'user', content: userMessage },
    { role: 'assistant', content: fullResponse }
  ])

  // Add streaming metadata
  turn.annotate({
    streaming: true,
    totalLatencyMs: Date.now() - startTime,
    firstTokenLatencyMs: /* track separately */
  })
} finally {
  await turn.finish()
}

Error Handling & Retries

Robust error handling pattern:

async function robustLLMCall(userMessage: string, maxRetries = 3) {
  const session = await Lumina.session.start()
  const turn = session.turn()

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await turn.wrapLLM(
        async () => {
          return await openai.chat.completions.create({
            model: 'gpt-4o',
            messages: [{ role: 'user', content: userMessage }],
          })
        },
        { model: 'gpt-4o', prompt_id: 'chat_v1' }
      )

      turn.setMessages([
        { role: 'user', content: userMessage },
        { role: 'assistant', content: response.choices[0].message.content || '' }
      ])

      // Annotate success
      turn.annotate({
        success: true,
        attemptNumber: attempt + 1
      })

      await turn.finish()
      return response

    } catch (error) {
      // Log error in turn
      turn.annotate({
        error: {
          message: error.message,
          type: error.constructor.name,
          attempt: attempt + 1
        }
      })

      // Last attempt - give up
      if (attempt === maxRetries - 1) {
        await turn.finish()
        throw error
      }

      // Exponential backoff
      await new Promise(resolve => setTimeout(resolve, 2 ** attempt * 1000))
    }
  }
}

Multi-Model Workflows

Tracking complex AI workflows:

const session = await Lumina.session.start()

// Step 1: Classification with fast model
const classificationTurn = session.turn()
try {
  const category = await classificationTurn.wrapLLM(
    async () => {
      return await openai.chat.completions.create({
        model: 'gpt-4o-mini',
        messages: [{ role: 'user', content: `Classify: ${userMessage}` }],
      })
    },
    { model: 'gpt-4o-mini', prompt_id: 'classify_v1' }
  )

  classificationTurn.annotate({ step: 'classification' })
  await classificationTurn.finish()

  // Step 2: Generation with powerful model
  const generationTurn = session.turn()
  const response = await generationTurn.wrapLLM(
    async () => {
      return await openai.chat.completions.create({
        model: 'gpt-4o',
        messages: [
          { role: 'system', content: `Category: ${category}` },
          { role: 'user', content: userMessage }
        ],
      })
    },
    { model: 'gpt-4o', prompt_id: 'generate_v1' }
  )

  generationTurn.annotate({
    step: 'generation',
    category
  })
  await generationTurn.finish()

} catch (error) {
  // Errors tracked per turn
}

RAG (Retrieval-Augmented Generation)

Tracking retrieval and context:

const turn = session.turn()

try {
  // Retrieve context
  const retrievalResults = await turn.recordTool(
    'vector_search',
    async () => {
      return await vectorDB.search(userMessage, { limit: 5 })
    },
    { type: 'vector_search', target: 'pinecone', version: 'v1' }
  )

  // Add retrieval metadata
  turn.addRetrieval({
    source: 'documentation',
    query: userMessage,
    results: retrievalResults.map(r => ({
      id: r.id,
      score: r.score,
      content: r.text.substring(0, 200) // Truncate for storage
    }))
  })

  // Generate with context
  const context = retrievalResults.map(r => r.text).join('\n\n')
  const response = await turn.wrapLLM(
    async () => {
      return await openai.chat.completions.create({
        model: 'gpt-4o',
        messages: [
          { role: 'system', content: `Context:\n${context}` },
          { role: 'user', content: userMessage }
        ],
      })
    },
    { model: 'gpt-4o', prompt_id: 'rag_v1' }
  )

  turn.setMessages([
    { role: 'user', content: userMessage },
    { role: 'assistant', content: response.choices[0].message.content || '' }
  ])

  turn.annotate({
    contextLength: context.length,
    documentsRetrieved: retrievalResults.length
  })

} finally {
  await turn.finish()
}

Framework Integration

React Integration

Context provider pattern:

// LuminaProvider.tsx
import { createContext, useContext, useEffect, useState } from 'react'
import { Lumina } from 'luminasdk'
import type { SessionCtx } from 'luminasdk'

const LuminaContext = createContext<{ session: SessionCtx | null }>({ session: null })

export function LuminaProvider({ children }: { children: React.ReactNode }) {
  const [session, setSession] = useState<SessionCtx | null>(null)

  useEffect(() => {
    // Initialize on mount
    Lumina.init({
      endpoint: process.env.NEXT_PUBLIC_LUMINA_ENDPOINT!,
      writeKey: process.env.NEXT_PUBLIC_LUMINA_KEY!,
      captureTranscript: CaptureTranscript.Full,
      uiAnalytics: createDummyProvider()
    })

    // Start session
    Lumina.session.start().then(setSession)
  }, [])

  return (
    <LuminaContext.Provider value={{ session }}>
      {children}
    </LuminaContext.Provider>
  )
}

export const useLumina = () => useContext(LuminaContext)

Usage in components:

// ChatComponent.tsx
import { useLumina } from './LuminaProvider'

export function ChatComponent() {
  const { session } = useLumina()
  const [messages, setMessages] = useState([])

  async function sendMessage(text: string) {
    if (!session) return

    const turn = session.turn()
    try {
      const response = await turn.wrapLLM(
        async () => await callLLM(text),
        { model: 'gpt-4o', prompt_id: 'chat' }
      )

      turn.setMessages([
        { role: 'user', content: text },
        { role: 'assistant', content: response }
      ])

      setMessages(prev => [...prev,
        { role: 'user', content: text },
        { role: 'assistant', content: response }
      ])
    } finally {
      await turn.finish()
    }
  }

  return <div>{/* Chat UI */}</div>
}

Next.js Integration

API route wrapper:

// app/api/chat/route.ts
import { Lumina } from 'luminasdk'
import { NextRequest, NextResponse } from 'next/server'

// Initialize once at module level
Lumina.init({
  endpoint: process.env.LUMINA_ENDPOINT!,
  writeKey: process.env.LUMINA_WRITE_KEY!,
  captureTranscript: CaptureTranscript.Masked,
  maskFn: (text) => text.replace(/\b[\w.-]+@[\w.-]+\.\w{2,}\b/g, '[EMAIL]')
})

export async function POST(req: NextRequest) {
  const { message, sessionId } = await req.json()

  // Get or create session
  const session = await Lumina.session.start()
  const turn = session.turn()

  try {
    const response = await turn.wrapLLM(
      async () => {
        return await openai.chat.completions.create({
          model: 'gpt-4o',
          messages: [{ role: 'user', content: message }],
        })
      },
      { model: 'gpt-4o', prompt_id: 'api_chat' }
    )

    const assistantMessage = response.choices[0].message.content || ''

    turn.setMessages([
      { role: 'user', content: message },
      { role: 'assistant', content: assistantMessage }
    ])

    await turn.finish()

    return NextResponse.json({
      message: assistantMessage,
      sessionId: session.sessionId
    })

  } catch (error) {
    await turn.finish()
    return NextResponse.json({ error: error.message }, { status: 500 })
  }
}

Express.js Integration

Middleware pattern:

// middleware/lumina.ts
import { Request, Response, NextFunction } from 'express'
import { Lumina } from 'luminasdk'

// Initialize once
Lumina.init({
  endpoint: process.env.LUMINA_ENDPOINT!,
  writeKey: process.env.LUMINA_WRITE_KEY!,
  captureTranscript: CaptureTranscript.Full
})

export async function luminaMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
) {
  // Attach session to request
  req.luminaSession = await Lumina.session.start()

  // Identify user if authenticated
  if (req.user) {
    await Lumina.identify(req.user.id, {
      email: req.user.email,
      name: req.user.name
    })
  }

  next()
}

// Route handler
app.post('/api/chat', luminaMiddleware, async (req, res) => {
  const turn = req.luminaSession.turn()

  try {
    const response = await turn.wrapLLM(
      async () => await callLLM(req.body.message),
      { model: 'gpt-4o', prompt_id: 'chat' }
    )

    turn.setMessages([
      { role: 'user', content: req.body.message },
      { role: 'assistant', content: response }
    ])

    res.json({ response })
  } finally {
    await turn.finish()
  }
})

Performance Optimization

Minimize Network Overhead

Batch configuration:

Lumina.init({
  // Increase buffer size for high-volume apps
  maxBatchBytes: 500_000, // 500KB (default: 100KB)

  // Reduce flush frequency for low-latency requirements
  flushIntervalMs: 2000, // 2s (default: 5s)

  // Adjust sampling for high-traffic apps
  sampling: {
    turn: 1.0,      // Always capture turns
    uiEvent: 0.1,   // 10% of UI events
    span: 0.5       // 50% of spans
  }
})

Async Finish for Better UX

Non-blocking finish calls:

// ✅ Good: Don't block user response on SDK flush
async function handleMessage(text: string) {
  const turn = session.turn()

  const response = await turn.wrapLLM(
    async () => await callLLM(text),
    { model: 'gpt-4o', prompt_id: 'chat' }
  )

  turn.setMessages([
    { role: 'user', content: text },
    { role: 'assistant', content: response }
  ])

  // Return immediately to user
  const userResponse = { message: response }

  // Finish in background (don't await)
  turn.finish().catch(err => console.error('Lumina flush failed:', err))

  return userResponse
}

Reduce Payload Size

Selective transcript capture:

// Only capture important turns
const turn = session.turn()

if (shouldCapture(userMessage)) {
  turn.setMessages([
    { role: 'user', content: userMessage },
    { role: 'assistant', content: response }
  ])
} else {
  // Still track metadata without transcript
  turn.annotate({ transcriptOmitted: true })
}

Common Pitfalls

Pitfall 1: Not Calling finish()

Problem:

// ❌ Events never sent
const turn = session.turn()
await turn.wrapLLM(/* ... */)
// Forgot to call finish() - data lost!

Solution:

// ✅ Always use try/finally
const turn = session.turn()
try {
  await turn.wrapLLM(/* ... */)
} finally {
  await turn.finish()  // Guaranteed to execute
}

Pitfall 2: Creating Too Many Sessions

Problem:

// ❌ New session for every turn
for (const message of messages) {
  const session = await Lumina.session.start() // Wrong!
  const turn = session.turn()
  // ...
}

Solution:

// ✅ One session for entire conversation
const session = await Lumina.session.start()
for (const message of messages) {
  const turn = session.turn()
  // ...
}

Pitfall 3: Blocking on Flush

Problem:

// ❌ Awaiting finish() adds latency to user response
const response = await generateResponse()
await turn.finish() // User waits for network flush
return response

Solution:

// ✅ Flush asynchronously
const response = await generateResponse()
turn.finish() // Don't await
return response

Pitfall 4: Not Handling Errors

Problem:

// ❌ SDK errors crash the app
await Lumina.identify(userId)

Solution:

// ✅ Graceful degradation
try {
  await Lumina.identify(userId)
} catch (err) {
  console.error('Lumina identify failed:', err)
  // App continues working
}

Testing Strategies

Unit Testing

Mock the SDK:

// __mocks__/luminasdk.ts
export const Lumina = {
  init: jest.fn(),
  session: {
    start: jest.fn(() => ({
      turn: jest.fn(() => ({
        wrapLLM: jest.fn((fn) => fn()),
        setMessages: jest.fn(),
        annotate: jest.fn(),
        finish: jest.fn(),
      }))
    }))
  },
  identify: jest.fn(),
  reset: jest.fn(),
  getDistinctId: jest.fn(() => 'test-user'),
  isAnonymous: jest.fn(() => false),
}

Test with mock:

import { Lumina } from 'luminasdk'

jest.mock('luminasdk')

test('tracks LLM call', async () => {
  const session = await Lumina.session.start()
  const turn = session.turn()

  await myFunction(turn)

  expect(turn.wrapLLM).toHaveBeenCalled()
  expect(turn.finish).toHaveBeenCalled()
})

Integration Testing

Use test environment:

// test-setup.ts
import { Lumina, CaptureTranscript } from 'luminasdk'

Lumina.init({
  endpoint: process.env.TEST_LUMINA_ENDPOINT || 'http://localhost:8080',
  writeKey: process.env.TEST_LUMINA_KEY || 'test_key',
  captureTranscript: CaptureTranscript.None, // Don't send transcripts in tests
  flushIntervalMs: 100, // Flush quickly for tests
})

Troubleshooting

SDK Not Sending Events

Check initialization:

console.log('Lumina initialized:', Lumina._isInitialized)

Verify endpoint:

curl -X POST http://your-endpoint/collect/sdk/events \
  -H "X-Api-Key: your-key" \
  -H "Content-Type: application/x-ndjson" \
  -d '{"source":"test","name":"ping"}'

Enable debug logging:

Lumina.init({
  // ... config
  debug: true // Logs all SDK operations
})

Events Not Appearing in Dashboard

Common causes:

  1. Wrong API key
  2. Wrong project ID
  3. Events not flushed yet (wait 5s or call manual flush)
  4. Workers not running on ingest server

Manual flush:

await Lumina.flush()

License

MIT


Support

For issues, questions, or feature requests:


What's Next?

This SDK is the foundation for Grounded Intelligence — a platform that actively learns from every user interaction across every data stream.

As we expand integrations beyond session replays (customer support, observability, payment events, user feedback), your product will:

  • Get hyper-personalized with dynamic content and experiences that adapt to each user's behavior
  • Get continuously smarter as your AI models learn from production signals daily — grounded in real usage, not synthetic benchmarks

If this resonates, we'd love to talk. Reach out at the links above.