Skip to content

feat(logs): public capture API (PostHog.logger + PostHogLogsConfig)#525

Open
turnipdabeets wants to merge 9 commits into
mainfrom
feat/logs-pr3
Open

feat(logs): public capture API (PostHog.logger + PostHogLogsConfig)#525
turnipdabeets wants to merge 9 commits into
mainfrom
feat/logs-pr3

Conversation

@turnipdabeets
Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Customers asked for the same Logs SDK posthog-ios and posthog-react-native ship. This adds the Android equivalent: a posthog.logger you 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/logs with the expected resource attributes.

📝 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

turnipdabeets and others added 2 commits May 21, 2026 17:34
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>
@turnipdabeets turnipdabeets requested a review from a team as a code owner May 21, 2026 21:39
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

posthog-android Compliance Report

Date: 2026-05-22 15:52:26 UTC
Duration: 2819ms

⚠️ 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 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.
turnipdabeets added a commit that referenced this pull request May 22, 2026
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

@marandaneto marandaneto requested a review from a team May 22, 2026 07:34
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants