Skip to content

chore(logs): internal plumbing for upcoming logs feature#520

Merged
turnipdabeets merged 4 commits into
mainfrom
feat/logs-pr2
May 20, 2026
Merged

chore(logs): internal plumbing for upcoming logs feature#520
turnipdabeets merged 4 commits into
mainfrom
feat/logs-pr2

Conversation

@turnipdabeets
Copy link
Copy Markdown
Contributor

@turnipdabeets turnipdabeets commented May 19, 2026

💡 Motivation and Context

Sets up the internal plumbing for the upcoming logs feature so that the
public-facing PR (next) is purely additive on top of a proven wire path.

Adds:

  • PostHogLogRecord and PostHogLogSeverity (both internal)
  • OTLP/JSON payload builder (PostHogLogsOTLP)
  • PostHogApi.sendLogs(...) — POSTs to /i/v1/logs?token=<apiKey>
  • EndpointSpec.logs(...) factory — wires a third PostHogQueue into
    PostHog.setup alongside events and replay

No public log() API in this PR — that lands in the follow-up,
along with PostHogLogsConfig (service.name / environment / beforeSend
hook), capture-time context snapshot (distinctId / sessionId / screen
name), and screen-name tracking via Application.ActivityLifecycleCallbacks.
That follow-up will carry the changeset and the version bump; this PR
ships zero runtime behavior change.

The logs queue is constructed but not started in setup() — nothing
can enqueue records yet, so spinning up the periodic flush timer would
waste a daemon thread. A one-line // logsQueue.start() marker sits in
PostHog.setup where it'll be uncommented when the public capture API
ships.

Worth noting for review:

  • Retry policy for /i/v1/logs is narrower than /batch /
    /snapshot: 408, 429, and 5xx only. 3xx redirects aren't retried
    (would loop on a misconfigured host). Regression-guarded so events
    don't accidentally inherit the broader set.
  • Wire format follows OTLP LogsService JSON
    (resourceLogs[].scopeLogs[].logRecords[]). Multi-record batches
    share a single resource + scope envelope, locked in by test.
  • ABI delta: one synthetic constructor parameter on private PostHog(...)
    (unreachable from outside the class) plus two @PostHogInternal
    accessors for logsStoragePrefix. No public surface change.

💚 How did you test it?

  • ./gradlew :posthog:test — OTLP shape, storage round-trip (both
    direct and through config.serializer), retry policy, multi-record
    batching, sendLogs 408 / 500, per-key collision rules,
    parameterized severity coverage
  • ./gradlew :posthog-android:testDebugUnitTest
  • ./gradlew :posthog:apiCheck — confirms ABI is additive
  • ./gradlew spotlessCheck detekt

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

Internal plumbing for the upcoming logs feature. Adds PostHogLogRecord,
PostHogLogSeverity, an OTLP/JSON payload builder, PostHogApi.sendLogs
posting to /i/v1/logs, and an EndpointSpec.logs factory wired through
PostHog.setup. Everything stays internal; no public log() API yet.

The logs queue is constructed but intentionally not started — nothing
can enqueue records, so the periodic flush timer would be wasted. A
one-line marker in PostHog.setup marks where logsQueue.start() goes
back in once the public capture API ships.

Retry policy for /i/v1/logs is narrower than /batch: 408, 429, and 5xx
only. Regression-guarded so events do not inherit the broader set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@turnipdabeets turnipdabeets requested a review from a team as a code owner May 19, 2026 21:27
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 19, 2026

⚠️ 2 packages modified but this PR has no changeset

This is informational — the PR is not blocked. Click the triangle above to collapse, or push a fix and this comment will auto-delete.

Modified in this PR but no changeset added:

  • posthog
  • posthog-android

If this change should ship, run pnpm changeset and select a bump level.
If it isn't user-facing (refactor with no behavior change, internal tooling, generated files), no action needed.

@turnipdabeets turnipdabeets changed the title feat(logs): record types, OTLP builder, /i/v1/logs endpoint chore(logs): internal plumbing for upcoming logs feature May 19, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 19, 2026

posthog-android Compliance Report

Date: 2026-05-20 13:54:30 UTC
Duration: 2822ms

⚠️ Some Tests Failed

0/16 tests passed, 16 failed


Feature_Flags Tests

⚠️ 0/16 tests passed, 16 failed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 286ms
Request Payload.Flags Request Uses V2 Query Param 21ms
Request Payload.Flags Request Hits Flags Path Not Decide 25ms
Request Payload.Flags Request Omits Authorization Header 28ms
Request Payload.Token In Flags Body Matches Init 16ms
Request Payload.Groups Round Trip 16ms
Request Payload.Groups Default To Empty Object 14ms
Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It 14ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 16ms
Request Payload.Disable Geoip Omitted Defaults To False 15ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 14ms
Request Lifecycle.No Flags Request On Init Alone 12ms
Request Lifecycle.No Flags Request On Normal Capture 2045ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 15ms
Request Lifecycle.Mock Response Value Is Returned To Caller 13ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 13ms

Failures

request_payload.request_with_person_properties_device_id

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

request_payload.flags_request_uses_v2_query_param

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

request_payload.flags_request_hits_flags_path_not_decide

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

request_payload.flags_request_omits_authorization_header

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

request_payload.token_in_flags_body_matches_init

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

request_payload.groups_round_trip

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

request_payload.groups_default_to_empty_object

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

request_payload.person_properties_distinct_id_auto_populated_when_caller_omits_it

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

request_payload.disable_geoip_false_propagates_as_geoip_disable_false

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

request_payload.disable_geoip_omitted_defaults_to_false

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

request_payload.flag_keys_to_evaluate_contains_only_requested_key

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

request_lifecycle.no_flags_request_on_init_alone

Expected 0 /flags requests, got 1

request_lifecycle.no_flags_request_on_normal_capture

Expected 0 /flags requests, got 1

request_lifecycle.two_flag_calls_produce_two_remote_requests

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

request_lifecycle.mock_response_value_is_returned_to_caller

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

side_effect_events.get_feature_flag_captures_feature_flag_called_event

404, message='Not Found', url='http://sdk-adapter:8080/get_feature_flag'

Comment thread posthog/src/main/java/com/posthog/logs/PostHogLogRecord.kt Outdated
Comment thread posthog/src/main/java/com/posthog/internal/logs/PostHogLogsOTLP.kt
Comment thread posthog/src/main/java/com/posthog/internal/logs/PostHogLogsOTLP.kt
- toAnyValue now ISO8601-encodes java.util.Date through the SDK's
  existing formatISO8601Date helper instead of falling through to the
  locale-dependent value.toString() path.
- nanosNow now takes a PostHogDateProvider (default
  PostHogDeviceDateProvider) so the public capture API in the
  follow-up can pass config.dateProvider and tests can drive
  timestamps via FakePostHogDateProvider.
- EndpointSpec.logs now layers os.name and os.version from
  PostHogContext into the resource attributes captured at factory
  time, narrowing the gap with iOS. service.* and
  deployment.environment still wait for the logs config in the
  follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@ioannisj ioannisj left a comment

Choose a reason for hiding this comment

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

LG, left one last comment but I'll leave that up to you.

Comment thread posthog/src/main/java/com/posthog/logs/PostHogLogRecord.kt Outdated
Drops the per-call PostHogDeviceDateProvider allocation. The function
now takes a nullable PostHogDateProvider and falls back to
System.currentTimeMillis when none is supplied, matching the
dateProvider?.currentTimeMillis() ?: System.currentTimeMillis()
convention used in PostHogSessionManager and TimeBasedEpochGenerator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@turnipdabeets turnipdabeets merged commit ae1fed2 into main May 20, 2026
14 checks passed
@turnipdabeets turnipdabeets deleted the feat/logs-pr2 branch May 20, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants