chore(logs): internal plumbing for upcoming logs feature#520
Conversation
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>
|
posthog-android Compliance ReportDate: 2026-05-20 13:54:30 UTC
|
| 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'
- 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>
ioannisj
left a comment
There was a problem hiding this comment.
LG, left one last comment but I'll leave that up to you.
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>
💡 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:
PostHogLogRecordandPostHogLogSeverity(bothinternal)PostHogLogsOTLP)PostHogApi.sendLogs(...)— POSTs to/i/v1/logs?token=<apiKey>EndpointSpec.logs(...)factory — wires a thirdPostHogQueueintoPostHog.setupalongside events and replayNo public
log()API in this PR — that lands in the follow-up,along with
PostHogLogsConfig(service.name / environment / beforeSendhook), 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()— nothingcan enqueue records yet, so spinning up the periodic flush timer would
waste a daemon thread. A one-line
// logsQueue.start()marker sits inPostHog.setupwhere it'll be uncommented when the public capture APIships.
Worth noting for review:
/i/v1/logsis 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.
LogsServiceJSON(
resourceLogs[].scopeLogs[].logRecords[]). Multi-record batchesshare a single resource + scope envelope, locked in by test.
private PostHog(...)(unreachable from outside the class) plus two
@PostHogInternalaccessors for
logsStoragePrefix. No public surface change.💚 How did you test it?
./gradlew :posthog:test— OTLP shape, storage round-trip (bothdirect and through
config.serializer), retry policy, multi-recordbatching,
sendLogs408 / 500, per-key collision rules,parameterized severity coverage
./gradlew :posthog-android:testDebugUnitTest./gradlew :posthog:apiCheck— confirms ABI is additive./gradlew spotlessCheck detekt📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file