Skip to main content
Tell has three data protocols. Choosing the right one for each signal keeps your data clean and your queries fast.

Events, logs, or snapshots?

SignalProtocolExample
A user does somethingEventsign_up, purchase, page_view
Your app does somethingLogError, warning, deploy, job completion
An external service has a metricSnapshotGitHub stars, Shopify revenue, Cloudflare requests
Events are user-centric. Every event is tied to a device and a user. Use events for anything you’d put in a product analytics tool — signups, purchases, feature usage, page views, funnels, retention. Logs are infrastructure-centric. They’re tied to a service and a severity level, not a user. Use logs for errors, warnings, deploys, background job status — anything your application does that isn’t a direct user action. Snapshots pull metrics from external services on a schedule. Use snapshots for data that lives outside your app — repository stars, store revenue, email delivery rates.

When it’s unclear

Some signals could go either way. Here’s how to decide:
SignalUse event or log?Why
User clicks “Export”EventIt’s a user action you want in funnels and retention
Export job failsLogIt’s an application error, not a user action
User sees an error pageLogIt’s an application error — count and filter by user in log queries
Server returns 500LogIt’s infrastructure behavior
Payment succeedsEvent (revenue)It’s a business outcome tied to a user
Payment gateway times outLogIt’s a system failure
API rate limit hitLogIt’s infrastructure, even if a user triggered it
The test: would a product manager care about this in a funnel or retention chart? If yes, it’s an event. If it’s something an engineer debugs in a log viewer, it’s a log.

Naming events

Use snake_case. Be specific. Name the action, not the UI element.
// Good
tell.track('sign_up', { plan: 'free' });
tell.track('invite_sent', { role: 'editor' });
tell.track('report_exported', { format: 'csv' });

// Bad
tell.track('click_button');        // what button?
tell.track('SignUp');              // use snake_case
tell.track('user_did_a_thing');   // be specific

Naming conventions

PatternExampleWhen to use
noun_verbreport_exported, invite_sentCompleted actions (most events)
noun_verbcheckout_started, trial_endedState transitions
page_viewpage_viewPage or screen views (use this exact name)
feature_usedfeature_used with feature propertyGeneric feature tracking
Keep your event namespace flat. Don’t use dots or slashes — purchase not ecommerce.purchase. Use properties for dimensions, not the event name.
// Good — one event name, dimensions in properties
tell.track('purchase', { category: 'subscription', plan: 'pro' });
tell.track('purchase', { category: 'one_time', product: 'report' });

// Bad — splitting into multiple event names
tell.track('subscription_purchase', { plan: 'pro' });
tell.track('one_time_purchase', { product: 'report' });
The first approach gives you one funnel step, one retention event, and one metric — broken down by category. The second fragments your data.

Choosing properties

Properties are the key-value pairs that make your events queryable. Every property you send becomes a breakdown, filter, or aggregation dimension.

What to include

Property typeExamplesWhy
What happenedplan, amount, format, sourceCore dimensions for breakdowns
Where it happenedpage_url, screen, sectionLocation context
Experiment contextvariant, experiment_idA/B test analysis
Business contextcurrency, coupon_code, referrerRevenue and attribution

What NOT to include

  • PII in plain text — use the redact transform or hash values before sending
  • High-cardinality IDs — don’t put request_id or trace_id in event properties (use logs for that)
  • Redundant context — the SDK already sends device type, OS, browser, and session ID automatically

Super properties

If you find yourself adding the same property to every track call, register it once:
tell.register({ app_version: '2.1.0', environment: 'production' });
Every subsequent event includes these properties automatically. See Events & Properties for details.

Structuring logs

Logs need a severity level and a message. The service name is set once when you initialize the SDK. Add structured data for anything you’d want to filter or search on.
tell.log('error', 'Payment gateway timeout', {
  gateway: 'stripe',
  duration_ms: 12340,
  retry_count: 3
});
Use a consistent service name for each SDK instance — billing, auth, api, worker. This becomes your primary filter dimension in log queries. See Logs for severity levels and SDK methods.

A starter tracking plan

Here’s a minimal set of events that covers most SaaS products:
EventPropertiesWhy
page_viewurl, title, referrerTraffic and navigation
sign_upplan, source, referrerAcquisition
loginmethodActivation and frequency
feature_usedfeature, contextEngagement
purchaseplan, amount, currencyRevenue
invite_sentroleVirality
export_createdformat, row_countValue delivery
Start small. You can always add events later — but renaming or removing them means losing historical continuity.

What’s next

  • Events & Properties — the full event model and SDK methods
  • Logs — structured logging with severity levels
  • Integrations — pull snapshots from external services
  • Filtering — how properties become filters and breakdowns