Skip to content

nri-prelude: thread an analytics callback through LogHandler#152

Merged
omnibs merged 2 commits into
trunkfrom
haskell-analytics-tracking
May 27, 2026
Merged

nri-prelude: thread an analytics callback through LogHandler#152
omnibs merged 2 commits into
trunkfrom
haskell-analytics-tracking

Conversation

@omnibs
Copy link
Copy Markdown
Member

@omnibs omnibs commented May 8, 2026

Large changeset but easy to review, I promise.

Context

We're introducing analytics events to our Haskell backend.

Compared to tracing spans, those events should have a one-notch-higher level of deliverability guarantees. It's not vital that they are delivered, but it shouldn't be easy to drop them.

We decided to go for a design where they are synchronously delivered at the time they're emitted, and where you don't have to carry around an AnalyticsHandler throughout the call stack.

This spells out changes in our Platform.

How we did it

Platform gets a track event :: Task err (), bypassing the need for threading a handler argument through every function that wants to emit. Matches the ergonomics of Log.withContext — ambient context carried by Task, no explicit handler in user code.

The mechanism is generic. Platform.Analytics.Internal.trackEvent takes any ToJSON value, opens an analytics.track child tracing span, attaches the JSON as span details, and invokes a LogHandler-carried Aeson.Value -> Task Never () callback. The callback runs as a Task against the surrounding LogHandler — so a wire-layer implementation can Log.error on delivery failure, open its own child spans, and generally participate in the existing observability pipeline. The Never error type encodes the contract that analytics failure never fails the outer track call; downstream is expected to swallow and log its own failures.

What changes

  • LogHandler gains a trackAnalyticsEvent :: Aeson.Value -> Task Never () field, propagated through mkHandler to every child handler.
  • rootTracingSpanIO gains the callback as a new parameter. Breaking change. silentTrack ships as the no-op default for callers who don't want analytics.
  • New module Platform.Analytics.Internal exposes trackEvent :: ToJSON e => e -> Task err (). The .Internal suffix signals it's not meant for direct product-code use; applications should wrap it behind a typed entry point that enforces their event dictionary at the type level. Haskell has no cross-package import fence, so the convention + downstream lint rules are the available enforcement.
  • Sibling packages that now use Platform.silentTrack (nri-kafka, nri-http, nri-redis, nri-postgresql, nri-observability) bump their nri-prelude bound to >= 0.7.0.0 && < 0.8. The rest just bump the upper bound to < 0.8. Also fixes nri-kafka's sync-write-benchmark stanza, which the previous hpack pass left at <0.7.
  • Bump to 0.7.0.0 to flag the breaking change.
  • Refreshes golden files: nri-prelude (version-bump string changes across 9.8/9.10/9.12) and nri-redis (source-line shifts from the new silentTrack arg in spanForTask/spanForFailingTask, plus the version-string bump after rebasing onto 1adf200).

Test plan

  • cabal build all green
  • cabal test nri-prelude — 278 passed; new test verifies the analytics.track child span carries the JSON payload as details, on top of the existing callback-invocation test
  • cabal test nri-redis — 100 passed; goldens regenerated for the new line numbers
  • Sibling packages build clean under the tightened bounds
  • NRI Monorepo builds and downstream API consumers work with these changes

🤖 PR drafted with Claude Code

@omnibs omnibs changed the title poc: analytics tracking Threads an analytics callback through LogHandler, adds nri-analytics May 26, 2026
@omnibs omnibs force-pushed the haskell-analytics-tracking branch from 0fdd622 to 34a5d26 Compare May 26, 2026 19:52
@omnibs omnibs changed the title Threads an analytics callback through LogHandler, adds nri-analytics nri-prelude: thread an analytics callback through LogHandler May 26, 2026
@omnibs omnibs force-pushed the haskell-analytics-tracking branch from 34a5d26 to cb97f14 Compare May 26, 2026 21:00
Adds an opaque (Aeson.Value -> IO ()) callback to LogHandler,
propagated by mkHandler to every child handler. rootTracingSpanIO
gains a new parameter for the callback; nullHandler defaults to
silentTrack. Re-exports silentTrack from Platform for callers.

This is a breaking change to rootTracingSpanIO and mkHandler. See
the design doc at NoRedInk/event-platform/docs/2026-05-07-haskell-analytics-tracking-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@omnibs omnibs force-pushed the haskell-analytics-tracking branch from cb97f14 to 236128f Compare May 26, 2026 21:37
@omnibs omnibs marked this pull request as ready for review May 26, 2026 21:54
Copilot AI review requested due to automatic review settings May 26, 2026 21:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an analytics callback pathway to nri-prelude tracing/log handling so Task code can emit JSON analytics events through the ambient LogHandler, while updating sibling packages for the breaking rootTracingSpanIO signature and refreshed golden outputs.

Changes:

  • Adds trackAnalyticsEventIO, silentTrack, and Platform.Analytics.Internal.trackEvent.
  • Updates rootTracingSpanIO/mkHandler call sites to pass an analytics callback.
  • Bumps nri-prelude to 0.7.0.0 and relaxes sibling package upper bounds.

Reviewed changes

Copilot reviewed 81 out of 81 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
nri-prelude/src/Platform/Internal.hs Adds analytics callback storage/propagation and updates root span API.
nri-prelude/src/Platform/Analytics/Internal.hs Adds internal analytics tracking task API.
nri-prelude/src/Platform.hs Re-exports silentTrack.
nri-prelude/src/Test/Internal.hs Updates internal test runner root span call.
nri-prelude/tests/PlatformSpec.hs Adds analytics callback tests and updates root span helper.
nri-prelude/tests/LogSpec.hs Updates mkHandler test helper.
nri-prelude/package.yaml Bumps version and exposes analytics module.
nri-prelude/nri-prelude.cabal Mirrors version/module updates.
nri-prelude/CHANGELOG.md Documents 0.7.0.0 changes.
nri-kafka/src/Kafka/Worker/Internal.hs Updates root tracing call with no-op analytics callback.
nri-kafka/src/Kafka/Worker/Partition.hs Updates message processing root tracing call.
nri-kafka/package.yaml Relaxes nri-prelude upper bound.
nri-kafka/nri-kafka.cabal Relaxes several nri-prelude bounds.
nri-http/test/Main.hs Updates test root tracing call.
nri-http/package.yaml Relaxes nri-prelude bounds.
nri-http/nri-http.cabal Mirrors bound updates.
nri-redis/test/Spec/Redis.hs Updates Redis observability test root spans.
nri-redis/package.yaml Relaxes nri-prelude bound.
nri-redis/nri-redis.cabal Mirrors bound updates.
nri-postgresql/test/ObservabilitySpec.hs Updates PostgreSQL observability test root span.
nri-postgresql/package.yaml Relaxes nri-prelude bound.
nri-postgresql/nri-postgresql.cabal Mirrors bound updates.
nri-observability/scripts/memory-leak-test/Main.hs Updates script root tracing call.
nri-observability/package.yaml Relaxes nri-prelude bound.
nri-observability/nri-observability.cabal Mirrors bound updates.
nri-env-parser/package.yaml Relaxes nri-prelude bound.
nri-env-parser/nri-env-parser.cabal Mirrors bound updates.
nri-log-explorer/package.yaml Relaxes nri-prelude bound.
nri-log-explorer/nri-log-explorer.cabal Mirrors bound update.
nri-test-encoding/package.yaml Relaxes nri-prelude bound.
nri-test-encoding/nri-test-encoding.cabal Mirrors bound updates.
nri-prelude/tests/golden-results-9.8/* Refreshes GHC 9.8 golden outputs for version/line changes.
nri-prelude/tests/golden-results-9.10/* Refreshes GHC 9.10 golden outputs for version changes.
nri-prelude/tests/golden-results-9.12/* Refreshes GHC 9.12 golden outputs for version changes.
nri-redis/test/golden-results-9.8/* Refreshes Redis golden source locations after call-site changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread nri-kafka/package.yaml Outdated
Comment thread nri-redis/package.yaml Outdated
Comment thread nri-http/package.yaml Outdated
Comment thread nri-postgresql/package.yaml Outdated
Comment thread nri-observability/package.yaml Outdated
Comment thread nri-prelude/src/Platform/Analytics/Internal.hs
@omnibs omnibs force-pushed the haskell-analytics-tracking branch from 236128f to 2bdf9f9 Compare May 26, 2026 22:26
trackEvent opens an analytics.track child span, attaches the JSON
payload as span details, and invokes the LogHandler's analytics
callback. The .Internal suffix signals do-not-import from product
code; consumers should wrap this in a typed entry point.

Bump to 0.7.0.0 (breaking: rootTracingSpanIO signature changed in
the previous commit). Bump nri-prelude upper bound to <0.8 in the
sibling packages so the workspace builds, and refresh hard-coded
package strings in the test-suite golden files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@omnibs omnibs force-pushed the haskell-analytics-tracking branch from 2bdf9f9 to 35bc0b8 Compare May 27, 2026 00:43
@omnibs omnibs enabled auto-merge May 27, 2026 03:32
@omnibs omnibs requested a review from ArthurJordao May 27, 2026 13:09
@omnibs omnibs added this pull request to the merge queue May 27, 2026
Merged via the queue into trunk with commit 99770e4 May 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants