Best Practices
Follow these best practices to get the most out of the Grounded Intelligence SDK.
1. Initialize Once
Always initialize at the application entry point, never in individual components or on every request:
// ✅ Good: Initialize at app startup
// main.ts or App.tsx
Lumina.init({ /* ... */ })
// ❌ Bad: Initialize on every request
function handleRequest() {
Lumina.init({ /* ... */ }) // Don't do this
}Why: Initialization sets up global state, event buffers, and provider connections. Reinitializing can cause:
- Lost events
- Multiple flush intervals
- Provider connection conflicts
2. Use One Session Per Conversation
// ✅ Good: One session per chat thread
const session = await Lumina.session.start()
// User sends first message
const turn1 = session.turn()
await turn1.wrapLLM(/* ... */)
await turn1.finish()
// User sends follow-up in same conversation
const turn2 = session.turn()
await turn2.wrapLLM(/* ... */)
await turn2.finish()
// ❌ Bad: New session for every turn
const session1 = await Lumina.session.start()
const turn1 = session1.turn()
await turn1.finish()
const session2 = await Lumina.session.start() // Don't do this
const turn2 = session2.turn()
await turn2.finish()Why: Sessions represent a conversation thread and share a trace ID. Creating a new session for every turn:
- Breaks conversation continuity
- Makes analytics harder
- Loses context across turns
Note: In most real-world apps, session.start() happens once (e.g., when a chat opens), and session.turn() is called each time the user sends a message. You don't typically iterate through messages in a loop - each turn happens as the user interacts.
3. Always Call finish()
Use try/finally blocks to guarantee finish() is called:
// ✅ Good: Use try/finally to guarantee finish
const turn = session.turn()
try {
await turn.wrapLLM(/* ... */)
turn.setMessages(/* ... */)
} finally {
await turn.finish() // Always executes, even on error
}
// ❌ Bad: finish() might not be called on error
const turn = session.turn()
await turn.wrapLLM(/* ... */)
await turn.finish() // Skipped if wrapLLM throwsWhy: finish() sends events to the ingest server. Without it:
- Events are never sent
- Analytics data is lost
- Memory leaks (events stay in buffer)
4. Identify Users Early
Identify users immediately after authentication:
// ✅ Good: Identify as soon as user logs in
async function handleLogin(userId: string, userInfo: any) {
await Lumina.identify(userId, {
email: userInfo.email,
name: userInfo.name,
plan: userInfo.plan
})
}
// ❌ Bad: Never identifying users
// Anonymous tracking is okay, but identified gives richer analyticsWhy: Early identification ensures:
- All subsequent events are linked to the user
- Anonymous → identified aliasing works correctly
- Better user journey analytics
5. Mask PII
Always use masked mode in production with a comprehensive maskFn:
// ✅ Good: Use Masked mode with custom maskFn
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
},
})
// ❌ Bad: No masking in production
Lumina.init({
captureTranscript: CaptureTranscript.Full, // Risky in production
})Why: PII exposure can lead to:
- Privacy violations
- GDPR/CCPA compliance issues
- Security breaches
6. Don't Block on Flush
Don't block user responses on SDK flush operations:
// ✅ 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
}
// ❌ Bad: Awaiting finish() adds latency to user response
async function handleMessage(text: string) {
const turn = session.turn()
const response = await generateResponse()
await turn.finish() // User waits for network flush
return response
}Why: Awaiting finish() adds network latency to the critical path. Let it run asynchronously.
7. Use Environment Variables
Store sensitive keys in environment variables, never hardcode:
# .env.local
NEXT_PUBLIC_LUMINA_ENDPOINT=https://ingest.yourdomain.com
NEXT_PUBLIC_LUMINA_WRITE_KEY=gi_xxxxx
NEXT_PUBLIC_POSTHOG_KEY=phc_xxxxx// ✅ Good: Use environment variables
Lumina.init({
endpoint: process.env.NEXT_PUBLIC_LUMINA_ENDPOINT!,
writeKey: process.env.NEXT_PUBLIC_LUMINA_WRITE_KEY!,
})
// ❌ Bad: Hardcoded keys
Lumina.init({
endpoint: 'https://ingest.example.com',
writeKey: 'gi_1234567890abcdef', // Don't do this
})8. Configure for Environment
Use different settings for dev/staging/prod:
const isDev = process.env.NODE_ENV === 'development'
Lumina.init({
endpoint: isDev
? 'http://localhost:8080'
: process.env.NEXT_PUBLIC_LUMINA_ENDPOINT!,
writeKey: process.env.NEXT_PUBLIC_LUMINA_WRITE_KEY!,
captureTranscript: isDev
? CaptureTranscript.Full
: CaptureTranscript.Masked,
flushIntervalMs: isDev ? 1000 : 5000, // More frequent in dev
})9. Annotate with Context
Add relevant context to turns for better analytics:
// ✅ Good: Rich annotations
turn.annotate({
ui_session_id: posthog.get_session_id(),
ui_replay_url: posthog.get_replay_url(),
user_tier: 'pro',
feature_flag: 'new_ui_enabled',
ab_test_variant: 'variant_b',
})
// ❌ Bad: No context
await turn.finish() // Minimal value for analyticsWhy: Annotations enable:
- Correlation with UI events
- Segmentation by user properties
- A/B test analysis
10. Minimize Payload Size
Be selective about what you capture:
// ✅ Good: Selective transcript capture
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 })
}
// ✅ Good: Truncate large context in retrieval
turn.addRetrieval({
source: 'docs',
query: userMessage,
results: retrievalResults.map(r => ({
id: r.id,
score: r.score,
content: r.text.substring(0, 200) // Truncate
}))
})
// ❌ Bad: Store entire documents
turn.addRetrieval({
results: retrievalResults.map(r => ({
content: r.text // Could be 10KB+ per result
}))
})Why: Large payloads:
- Increase network overhead
- Slow down analytics queries
- Increase storage costs
Common Pitfalls
Pitfall 1: Not Calling finish()
// ❌ Events never sent
const turn = session.turn()
await turn.wrapLLM(/* ... */)
// Forgot to call finish() - data lost!Solution: Use try/finally (see #3 above)
Pitfall 2: Creating Too Many Sessions
// ❌ New session for every turn
for (const message of messages) {
const session = await Lumina.session.start() // Wrong!
const turn = session.turn()
// ...
}Solution: One session per conversation (see #2 above)
Pitfall 3: Blocking on Flush
// ❌ Awaiting finish() adds latency
const response = await generateResponse()
await turn.finish() // User waits for network flush
return responseSolution: Flush asynchronously (see #6 above)
Pitfall 4: Not Handling SDK Errors
// ❌ SDK errors crash the app
await Lumina.identify(userId)Solution: Graceful degradation
// ✅ Graceful degradation
try {
await Lumina.identify(userId)
} catch (err) {
console.error('Lumina identify failed:', err)
// App continues working
}Performance Tips
1. Minimize Network Overhead
Adjust batch configuration for your traffic:
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)
})2. Reduce Payload Size
Only capture important turns:
const turn = session.turn()
if (isImportantTurn(userMessage)) {
turn.setMessages([/* ... */])
} else {
turn.annotate({ transcriptOmitted: true })
}3. Sample High-Traffic Events
For very high-traffic applications:
Lumina.init({
sampling: {
turn: 1.0, // Always capture turns
uiEvent: 0.1, // 10% of UI events
span: 0.5 // 50% of spans
}
})Next Steps
- Troubleshooting - Common issues and solutions
- Framework Integration - Framework-specific patterns
- Advanced Features - Streaming, error handling, RAG