feat(logs): public capture API (PostHog.logger + PostHogLogsConfig)#525
feat(logs): public capture API (PostHog.logger + PostHogLogsConfig)#525turnipdabeets wants to merge 9 commits into
Conversation
Surfaces the logs facade for app developers:
- PostHog.logger.trace/debug/info/warn/error/fatal(message, attrs?)
- PostHog.logger.log(message, severity, attrs?) for runtime severities
- PostHogLogsConfig: serviceName, serviceVersion, environment,
resourceAttributes, flushIntervalSeconds, flushAt, maxBatchSize,
maxBufferSize, rateCapMaxLogs, rateCapWindowSeconds
- addBeforeSend / removeBeforeSend hooks (matches events shape)
- PostHogLogRecord data class, PostHogLogSeverity enum
Capture-time auto-attached attributes: app.state, distinctId, sessionId,
screen.name, feature_flags. Resource attributes: telemetry.sdk.{name,
version}, service.{name, version}, deployment.environment, os.{name,
version}. service.name falls back to $app_namespace then "unknown_service"
per OTel spec.
Rate cap (500 / 10s) matches iOS + RN. CopyOnWriteArrayList for the
beforeSend hook list, lock-free on the capture hot path. Per-hook
try/catch mirrors events (a throwing hook drops the record). Catch-all
in captureLog logs only the throwable class, not the message, to avoid
leaking PII embedded in exception messages.
Sample app: NormalActivity gains a Logs button that launches the new
LogsActivity with six severity rows + manual flush. MainActivity removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
posthog-android Compliance ReportDate: 2026-05-22 15:52:26 UTC
|
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ❌ | 285ms |
| Request Payload.Flags Request Uses V2 Query Param | ❌ | 21ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ❌ | 21ms |
| Request Payload.Flags Request Omits Authorization Header | ❌ | 32ms |
| Request Payload.Token In Flags Body Matches Init | ❌ | 18ms |
| Request Payload.Groups Round Trip | ❌ | 17ms |
| 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 | ❌ | 15ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ❌ | 16ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ❌ | 15ms |
| Request Lifecycle.No Flags Request On Init Alone | ❌ | 10ms |
| Request Lifecycle.No Flags Request On Normal Capture | ❌ | 2046ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ❌ | 14ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ❌ | 14ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ❌ | 15ms |
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'
Developers enable debug to audit what the SDK is shipping; suppressing the logs payload only blinds them to PII in their own log bodies. Events already logs the full payload in debug -- match that behavior. Logcat is local-device only, never transmitted.
Declares posthog-android and posthog-server as transitive re-exporters
of posthog so the hygiene check stops flagging them as 'extra' when
declared in a changeset alongside posthog without their own source
changes.
Both modules declare api(project(":posthog")) in Gradle and must
republish to deliver upstream core changes to their Maven consumers.
Config schema added in PostHog/.github#42.
Triggered by warning on #525.
| } | ||
|
|
||
| // Cache for capture-time context snapshot on log records. | ||
| this.lastScreenName = screenTitle |
There was a problem hiding this comment.
now you can do this #119 as a follow up
we need to make this clear that this is only if they call screen or if they use screen auto capture
The test set `config.flushAt = 1` to drop the threshold, but `EndpointSpec.logs` reads `it.logs.flushAt` (default 20) since the PostHogLogsConfig split — so with 1 record queued the threshold was never hit and the request never fired. Set `config.logs.flushAt`. This was the source of the Build Job hangs on CI — the test in question was waiting forever for an HTTP request that the queue would never send.
Same bug as c6b99e7 — the two logs-factory tests for os attribute wiring also still set events flushAt instead of logs.flushAt, so the queue never flushed and the test waited indefinitely on a request that never went out. Caught by running the full class locally.
💡 Motivation and Context
Customers asked for the same Logs SDK posthog-ios and posthog-react-native ship. This adds the Android equivalent: a
posthog.loggeryou can call from anywhere, with batching, offline persistence, redaction hooks, and a rate cap. Stacked on #520 (internal plumbing, already merged).💚 How did you test it?
Unit tests cover capture, severity routing, OTLP wire format, rate cap, and beforeSend hooks. End-to-end tested on a physical device — 7 scenarios including all severities, app background/foreground, offline → online recovery, force-stop persistence, manual flush, and 20+ tap stress. Records land on
/i/v1/logswith the expected resource attributes.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file