Event Design Principles
Structure Your Events Consistently
Use a consistent structure across your application:
Good Structure {
"name" : "user_prompt" ,
"session_id" : "conv_abc123" ,
"user_id" : "user_456" ,
"metadata" : {
"prompt" : "actual user input"
}
}
Avoid {
"name" : "event" ,
"metadata" : {
"data" : "mixed content" ,
"stuff" : "unclear purpose"
}
}
Maintain Session Continuity
The session_id connects events into a conversation journey. Turret auto-generates it if not provided, but you must persist and reuse it:
class TurretTracker {
constructor ( apiKey ) {
this . apiKey = apiKey ;
this . sessionId = null ;
this . userId = null ;
}
async track ( prompt ) {
const response = await fetch ( 'https://api.useturret.com/track' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'X-API-Key' : this . apiKey
},
body: JSON . stringify ({
name: 'user_message' ,
session_id: this . sessionId , // null on first call
user_id: this . userId ,
metadata: { prompt }
})
});
const data = await response . json ();
// Store for subsequent messages
this . sessionId = data . session_id ;
this . userId = data . user_id ;
}
startNewConversation () {
this . sessionId = null ; // Next track() gets new session_id
}
}
Already have conversation IDs? Pass your existing conversation/thread ID as session_id - Turret will use it directly instead of generating one.
Focus on User-Generated Text
Turret works best with free-form text that users actually write or say:
Excellent for Turret:
User prompts and questions
Search queries
Support requests
Feedback and comments
Less valuable for Turret:
System-generated messages
Enum values or categories
Technical logs
Numeric data
Session ID Best Practices
Option 1: Let Turret Generate IDs
If you don’t have existing conversation IDs, let Turret generate them:
// First message - no session_id
const response1 = await turret . track ({ prompt: 'What pricing plans do you offer?' });
const sessionId = response1 . session_id ; // Store this!
// Subsequent messages - include session_id
await turret . track ({ prompt: 'What features are in the pro plan?' , session_id: sessionId });
await turret . track ({ prompt: 'Can I get a discount?' , session_id: sessionId });
This creates a journey: Pricing → Features → Discounts
Option 2: Use Your Own IDs
If you already have conversation IDs (most apps do), pass them directly:
// Your app already has a conversation ID
const myConversationId = conversation . id ;
// Pass it to Turret - it uses your ID, not a generated one
await turret . track ({
prompt: 'What pricing plans do you offer?' ,
session_id: myConversationId ,
user_id: currentUser . id
});
What to Avoid
// BAD: Different session_id for each message (breaks journeys)
await turret . track ({ prompt: 'Question 1' }); // Gets session A
await turret . track ({ prompt: 'Question 2' }); // Gets session B - NOT connected!
// BAD: Hardcoded session_id (mixes all users together)
await turret . track ({ prompt: 'Question' , session_id: 'default' });
// GOOD: Store and reuse session_id
const { session_id } = await turret . track ({ prompt: 'Question 1' });
await turret . track ({ prompt: 'Question 2' , session_id }); // Connected!
Cross-Session Analysis with user_id
Track the same user across multiple conversations:
// Monday: User starts a conversation (new session)
const { user_id } = await turret . track ({ prompt: 'How do I set up the integration?' });
// Store user_id with your user record
// Wednesday: Same user, new conversation
await turret . track ({
prompt: 'The integration stopped working' ,
user_id: storedUserId // Same user, but new session_id (omitted = auto-generated)
});
This reveals patterns like: users who ask about setup often return with issues.
Focus on the text you want to cluster:
{
"name" : "user_prompt" ,
"session_id" : "conv_abc123" ,
"metadata" : {
"prompt" : "How do I integrate your API with my React app?"
}
}
Add Context When Helpful
You can include additional metadata for context (only the prompt field is clustered):
{
"name" : "user_prompt" ,
"session_id" : "conv_abc123" ,
"user_id" : "user_456" ,
"metadata" : {
"prompt" : "How do I integrate your API with my React app?" ,
"detected_language" : "english" ,
"platform" : "web"
}
}
Use Natural Language
Store text as users actually wrote it:
Preserve Original {
"prompt" : "hey, can u help me fix this bug?"
}
Avoid Over-Processing {
"prompt" : "HELP FIX BUG"
}
Event Timing
Track at the Right Moments
Capture events when you have the most complete information:
Track immediately after user submits text (prompts, questions, search queries)
Track after generating a response if you want to analyze AI outputs
Track when users provide explicit feedback or ratings
Data Quality
Validate Before Sending
Ensure your events contain meaningful data:
function validateEvent ( name , sessionId , metadata ) {
// Check for required fields
if ( ! name || ! metadata ) return false ;
// Ensure metadata has text content
const hasText = Object . values ( metadata ). some ( value =>
typeof value === 'string' && value . trim (). length > 0
);
if ( ! hasText ) return false ;
// Warn if session_id is missing (recommended field)
if ( ! sessionId ) {
console . warn ( 'session_id missing - journey tracking disabled' );
}
return true ;
}
Handle Sensitive Data
Be careful with personally identifiable information (PII):
function sanitizeMetadata ( metadata ) {
return Object . fromEntries (
Object . entries ( metadata ). map (([ key , value ]) => [
key ,
typeof value === 'string' ?
value . replace ( / \b \d {3} - \d {2} - \d {4} \b / g , '[SSN]' )
. replace ( / \b [ A-Za-z0-9._%+- ] + @ [ A-Za-z0-9.- ] + \. [ A-Z|a-z ] {2,} \b / g , '[EMAIL]' )
: value
])
);
}
Use Async Tracking
Don’t block your main application flow:
async function trackEventAsync ( name , sessionId , userId , metadata ) {
// Use setTimeout to avoid blocking
setTimeout ( async () => {
try {
await trackEvent ( name , sessionId , userId , metadata );
} catch ( error ) {
console . error ( 'Event tracking failed:' , error );
// Don't let tracking failures break your app
}
}, 0 );
}
Implement Circuit Breakers
Protect your application from API failures:
class EventTracker {
constructor () {
this . failures = 0 ;
this . maxFailures = 5 ;
this . resetTime = 60000 ; // 1 minute
this . lastFailureTime = 0 ;
}
async trackEvent ( name , sessionId , userId , metadata ) {
if ( this . isCircuitOpen ()) {
console . log ( 'Circuit breaker open, skipping event' );
return ;
}
try {
await this . sendEvent ( name , sessionId , userId , metadata );
this . onSuccess ();
} catch ( error ) {
this . onFailure ();
throw error ;
}
}
isCircuitOpen () {
return this . failures >= this . maxFailures &&
Date . now () - this . lastFailureTime < this . resetTime ;
}
onSuccess () {
this . failures = 0 ;
}
onFailure () {
this . failures ++ ;
this . lastFailureTime = Date . now ();
}
}
Analytics Strategy
Start Simple, Evolve
Begin with basic events and add complexity over time:
Phase 1: Minimal (Just Track Messages)
{
"name" : "user_message" ,
"metadata" : {
"prompt" : "user's actual question or request"
}
}
Store the returned session_id for subsequent messages.
Phase 2: Connect to Your User System
{
"name" : "user_message" ,
"session_id" : "your-conversation-id" ,
"user_id" : "your-user-id" ,
"metadata" : {
"prompt" : "user's actual question or request"
}
}
Phase 3: Additional Context
{
"name" : "user_message" ,
"session_id" : "your-conversation-id" ,
"user_id" : "your-user-id" ,
"metadata" : {
"prompt" : "user's actual question or request" ,
"platform" : "mobile" ,
"language" : "en"
}
}
Monitor Data Quality
Regularly check that your events are providing value:
Topic diversity : Are you seeing varied, meaningful topics?
Journey insights : Are session_ids producing useful journey patterns?
Event distribution : Are some event types dominating?
Actionability : Can you make product decisions from the insights?
Common Patterns
The Chat Application Pattern
Track each message in a conversation:
class ChatTracker {
constructor ( turretApiKey ) {
this . turret = new TurretClient ( turretApiKey );
}
async onUserMessage ( userMessage ) {
// Track the user's message
await this . turret . track ({
name: 'user_message' ,
session_id: this . sessionId , // null on first message
user_id: this . userId ,
metadata: { prompt: userMessage }
}). then ( data => {
this . sessionId = data . session_id ;
this . userId = data . user_id ;
});
}
startNewConversation () {
this . sessionId = null ; // New conversation = new session
}
}
The Search Application Pattern
Track search queries - each search session is a conversation:
// Each search session gets its own session_id
const { session_id } = await turret . track ({
name: 'search_query' ,
metadata: { query: searchTerm }
});
// If user refines their search, include session_id
await turret . track ({
name: 'search_query' ,
session_id ,
metadata: { query: refinedSearchTerm }
});
The Support Ticket Pattern
Use ticket ID as session_id for easy correlation:
await turret . track ({
name: 'support_message' ,
session_id: ticketId , // Your existing ticket ID
user_id: customerId , // Your existing customer ID
metadata: { prompt: customerMessage }
});
Measuring Success
Key Metrics to Track
Topic insights actioned : How many topic discoveries lead to product changes?
Journey optimization : Have journey insights improved user flows?
User satisfaction correlation : Do insights correlate with satisfaction improvements?
Support ticket reduction : Are you catching issues before they become support requests?
Regular Review Process
Weekly : Review new and growing topics, check journey patterns
Monthly : Analyze topic trends and journey correlations
Quarterly : Assess impact of insights-driven decisions
Annually : Review overall analytics strategy and goals
The most successful teams treat Turret as a continuous insight engine. Combining topic clustering with journey tracking reveals powerful patterns that neither could surface alone.