In the previous chapter, Public Analytics Facade, we learned how to drop a "letter" (an event) into a mailbox. We established that the Facade is just a temporary holding area (a queue) that waits for the real machinery to start up.
In this chapter, we will meet that machinery: The Analytics Sink.
If the Facade is the mailbox, the Analytics Sink is the central Sorting Room at the post office.
When the sorting room opens for business (initializes), it empties the mailbox. For every letter that comes in, the Sorting Room has to make three decisions:
The Sink is the Router. It takes one input (your event) and decides which pipelines to send it to.
Before we look at code, let's visualize the life of an event once it reaches the Sink.
The Sink doesn't exist when the app first launches. We have to create it and "attach" it to the Facade. This usually happens in the application's startup file (main.tsx or similar).
Here is the code that brings the Sink to life:
// sink.ts
import { attachAnalyticsSink } from './index.js' // The Facade
export function initializeAnalyticsSink(): void {
// Connect the "Sorting Room" to the "Mailbox"
attachAnalyticsSink({
logEvent: logEventImpl,
logEventAsync: logEventAsyncImpl,
})
}
What happened here?
We called attachAnalyticsSink and passed it our logic (logEventImpl). As soon as this runs, any events waiting in the Facade's queue are immediately flushed into the logic we describe below.
This is the heart of the router. logEventImpl receives an event and decides what to do with it.
First, we check if the event is "sampled." Sampling is a technique used to save money and storage. If an event happens 1 million times a day, we might only want to record 1% of them to get the general idea.
// Inside logEventImpl...
// 1. Check if we should keep this event
const sampleResult = shouldSampleEvent(eventName)
// If result is 0, the event is dropped completely
if (sampleResult === 0) {
return
}
// Otherwise, record the sample rate (e.g., 1 out of 10)
const metadataWithSampleRate = { ...metadata, sample_rate: sampleResult }
Next, we check if we should send this to Datadog (our external dashboard tool). This involves checking a "Killswitch" (explained below) and Feature Gates.
// Inside logEventImpl...
if (shouldTrackDatadog()) {
// Datadog is external, so we strip internal secrets (_PROTO fields)
const safeMetadata = stripProtoFields(metadataWithSampleRate)
// Send to Datadog Pipeline
trackDatadogEvent(eventName, safeMetadata)
}
We will cover the specific details of Datadog in Datadog Integration.
Finally, we almost always send data to our own internal system. This is our "source of truth."
// Inside logEventImpl...
// Send to Internal Pipeline
// We send the full metadata here, including internal debug info
logEventTo1P(eventName, metadataWithSampleRate)
We will cover the internal pipeline in First-Party (Internal) Telemetry Pipeline.
Sometimes, things break. Maybe the Datadog API is down, or we are logging too much data and crashing the network.
We use Killswitches to instantly stop the Router from sending data to specific destinations without deploying new code.
The router uses a helper function isSinkKilled:
// sinkKillswitch.ts
export function isSinkKilled(sink: SinkName): boolean {
// Check our dynamic config (GrowthBook)
const config = getDynamicConfig(SINK_KILLSWITCH_CONFIG_NAME, {})
// If config says { datadog: true }, this returns true
return config?.[sink] === true
}
When shouldTrackDatadog() is called in the routing logic, it checks this switch first. If the switch is "On" (meaning killed), the router skips Datadog entirely.
The Analytics Sink is the brain of our telemetry. It ensures that:
However, you might have noticed we talked about metadata (the data inside the event). Sometimes, raw data isn't enough. We need to know who sent the event, or what version of the app they are running.
In the next chapter, we will learn how we automatically add this extra information to every single event.
Next Chapter: Metadata & Context Enrichment
Generated by Code IQ