Docs Trail

Send product analytics to Elk

Trail receives product analytics over the Segment HTTP tracking API. If your app already sends events to Segment, RudderStack or Jitsu, pointing it at Elk is a one-line change. If it sends nothing yet, you install a stock open-source SDK — not ours.

There is no Elk SDK to install, and there never will be. Trail speaks a published protocol on purpose. You keep whatever client you already trust; Elk is just a different URL and key. Nothing about your app becomes Elk-specific, and pointing it somewhere else later is the same one-line change.

What you need from us

Two values, which we issue per app:

Endpointhttps://signal-receiver-709757975936.us-west1.run.app/r/<signal-id>
Write keyAn opaque string. Sent as the HTTP Basic username, with an empty password — exactly as Segment does it.

Your SDK appends the protocol path (/v1/batch, /v1/track, …) to the endpoint itself. You only ever configure the base.

If you already send to Segment

Change the host and the write key. Nothing else — not the events, not the call shapes, not the SDK.

Node — @segment/analytics-node

import { Analytics } from '@segment/analytics-node'

const analytics = new Analytics({
  writeKey: process.env.ELK_WRITE_KEY,
  host: 'https://signal-receiver-709757975936.us-west1.run.app',
  path: '/r/<signal-id>/v1/batch',
})

analytics.track({ userId: 'user-123', event: 'Match Started' })

Browser — analytics.js

analytics.load('<write-key>', {
  integrations: {
    'Segment.io': {
      apiHost: 'signal-receiver-709757975936.us-west1.run.app/r/<signal-id>/v1',
    },
  },
})

Python — analytics-python

import segment.analytics as analytics

analytics.write_key = "<write-key>"
analytics.host = "https://signal-receiver-709757975936.us-west1.run.app/r/<signal-id>"
analytics.gzip = False   # see "Turn compression off" below

analytics.track("user-123", "Match Started")

If you send nothing yet

Install any Segment-compatible SDK and use it normally. The call vocabulary is Segment's, and all of it is supported:

trackSomething happened. Needs an event name.
identifyThis anonymous visitor is this user. Merges their history.
page / screenA view happened.
groupThis user belongs to this account or team.
aliasThese two identities are the same person.

Send identify as soon as you know who someone is, with the same anonymousId the earlier events carried. That is what stitches a signed-out session to the account it became — without it, the journey starts at login and the interesting half is missing.

No SDK at all

curl -X POST "https://signal-receiver-709757975936.us-west1.run.app/r/<signal-id>/v1/batch" \
  -u "<write-key>:" \
  -H 'Content-Type: application/json' \
  -d '{"batch":[
        {"type":"track","event":"Match Started","userId":"user-123",
         "messageId":"evt-1","timestamp":"2026-08-12T09:00:00Z"}
      ]}'

Confirming it worked

A successful call returns 200 with a body:

{"success":true,"accepted":2}

If some events in a batch were unusable, you still get 200 — the batch was fine, those items weren't — and the body says which and why:

{"success":true,"accepted":2,
 "dropped":[{"index":2,"id":"evt-3","reason":"track call has no event"}]}

Read that field. It is the difference between "analytics are working" and "analytics are working for two thirds of your events". Log it in whatever wraps your SDK.

200Accepted. Check dropped.
400The body was unusable — not JSON, empty batch, over the cap. Retrying will not help.
401Wrong write key.
403The Signal is paused. Talk to us.
404Wrong signal id in the URL.
413Body too large. Send smaller batches.
415Compressed body — see below.
429 / 503Back off and retry; Retry-After is set.
500Ours. Retry — duplicate delivery is safe.

Things that will bite you

Turn compression off. We refuse compressed bodies with a 415 rather than silently mangling them. RudderStack's .NET SDK gzips by default from v2.0.0 — pass gzip: false. Segment's Node SDK never compresses; analytics-python only does if you ask. If you hit this, the 415 names the cause and the fix.
A compression-stuck sender looks like silence, not failure. The 415 happens before we read the body, so there is no delivery to show you and the integration simply looks quiet. If a new integration shows nothing at all, check your sender's own logs first.

Send a messageId. Every stock SDK does. It is how a retried delivery is recognised as the same event instead of becoming a duplicate. Without one we derive a stable hash of the payload, which works but is weaker.

Retries are safe. Redelivering an identical batch overwrites rather than duplicates, so retry on 5xx freely. Do not retry 400 or 401 — nothing about them improves.

Browser events expose the write key. That is inherent to client-side analytics and true of Segment too, but the key is append-only ingest for one Signal. If you would rather not ship it at all, post events to your own backend route and forward them server-side.

What you get

Events land as rows you can query, keyed so that a retry cannot duplicate them. From there, sessionization, identity resolution, funnels, retention and DAU/WAU are SQL views over those rows — Elk materializes them when someone asks a question, rather than making you build them in a dashboard tool first.

You do not need to model any of that up front. Send the events; the questions come later.