diff --git a/posthog/tasks/test/test_usage_report.py b/posthog/tasks/test/test_usage_report.py index 20b6bf51d112..9024a60fafa5 100644 --- a/posthog/tasks/test/test_usage_report.py +++ b/posthog/tasks/test/test_usage_report.py @@ -3152,10 +3152,11 @@ def test_logs_per_sdk_usage_metrics(self, billing_task_mock: MagicMock, posthog_ ) lines = "" + lines += self._logs_records_json(self.org_1_team_1.id, "web", 3) lines += self._logs_records_json(self.org_1_team_1.id, "posthog-ios", 2) lines += self._logs_records_json(self.org_1_team_1.id, "posthog-android", 1) + lines += self._logs_records_json(self.org_1_team_1.id, "posthog-ruby", 7) lines += self._logs_records_json(self.org_1_team_2.id, "posthog-react-native", 4) - # A server SDK and an infra log (no telemetry.sdk.name) must not be counted. lines += self._logs_records_json(self.org_1_team_2.id, "posthog-node", 5) lines += self._logs_records_json(self.org_1_team_2.id, None, 6) # Team 3 has log records but no app_metrics2 row, so the pre-filter excludes it entirely. @@ -3174,9 +3175,9 @@ def test_logs_per_sdk_usage_metrics(self, billing_task_mock: MagicMock, posthog_ # telemetry.sdk.name are not counted; flutter ships no logs yet; team 5 has log records but # no app_metrics2 row, so the pre-filter drops it entirely. expected_counts: dict[str, tuple[dict, dict[str, int]]] = { - "org": (org_1_report, {"ios": 2, "react_native": 4, "android": 1, "flutter": 0}), - "team 3": (org_1_report["teams"]["3"], {"ios": 2, "android": 1, "react_native": 0}), - "team 4": (org_1_report["teams"]["4"], {"react_native": 4, "ios": 0}), + "org": (org_1_report, {"web": 3, "ios": 2, "react_native": 4, "android": 1, "flutter": 0, "ruby": 7}), + "team 3": (org_1_report["teams"]["3"], {"web": 3, "ios": 2, "android": 1, "react_native": 0, "ruby": 7}), + "team 4": (org_1_report["teams"]["4"], {"react_native": 4, "ios": 0, "web": 0, "ruby": 0}), "team 5": (org_1_report["teams"]["5"], {"ios": 0}), } for scope, (counters, per_sdk) in expected_counts.items(): diff --git a/posthog/tasks/usage_report.py b/posthog/tasks/usage_report.py index f561276c46d0..9f14d1fe6fc5 100644 --- a/posthog/tasks/usage_report.py +++ b/posthog/tasks/usage_report.py @@ -269,11 +269,12 @@ class UsageReportCounters: logs_retention_90d_mb_in_period: int # Per-SDK split of logs_records_in_period, which on its own has no SDK dimension. Keyed off the # telemetry.sdk.name resource attribute each SDK sets on every record. See SDK_TELEMETRY_NAMES. - # Web (browser) is intentionally absent: posthog-js doesn't set telemetry.sdk.name on logs yet. + web_logs_records_in_period: int ios_logs_records_in_period: int react_native_logs_records_in_period: int android_logs_records_in_period: int flutter_logs_records_in_period: int + ruby_logs_records_in_period: int # Instance metadata to be included in overall report @@ -2020,15 +2021,16 @@ def get_teams_with_logs_records_in_period( ) -# Maps the `telemetry.sdk.name` resource attribute (the mobile SDK package name, set on every log -# record) to the report field suffix used in `UsageReportCounters` / `_get_all_usage_data` keys. -# The browser SDK (posthog-js) is omitted: it doesn't set telemetry.sdk.name on logs (it only sets -# the OTLP scope name), so a "web" entry here would never match. Add it once posthog-js is fixed. +# Maps the `telemetry.sdk.name` resource attribute (the SDK identifier, set on every log record) to +# the report field suffix used in `UsageReportCounters` / `_get_all_usage_data` keys. posthog-js sets +# `web`; posthog-rails sets `posthog-ruby`; the mobile SDKs set their package name. SDK_TELEMETRY_NAMES: dict[str, str] = { + "web": "web", "posthog-ios": "ios", "posthog-react-native": "react_native", "posthog-android": "android", "posthog-flutter": "flutter", + "posthog-ruby": "ruby", } @@ -2043,8 +2045,9 @@ def get_teams_with_sdk_logs_records_in_period( Returns log record counts grouped by team and PostHog SDK, for the given period. The result is keyed by the short SDK suffix used on `UsageReportCounters` - (`ios`, `react_native`, `android`, `flutter`); each value is a list of - `(team_id, count)` tuples ready for `convert_team_usage_rows_to_dict`. + (the values of `SDK_TELEMETRY_NAMES` — `web`, `ios`, `react_native`, + `android`, `flutter`, `ruby`); each value is a list of `(team_id, count)` + tuples ready for `convert_team_usage_rows_to_dict`. `team_ids_with_logs` must be the team_ids that produced any log records in the same period (typically the result of `get_teams_with_logs_records_in_period`). It's used as a primary-key @@ -2487,10 +2490,12 @@ def _get_all_usage_data(period_start: datetime, period_end: datetime) -> dict[st "teams_with_logs_retention_30d_bytes_in_period": logs_retention_by_tier["30d"], "teams_with_logs_retention_90d_bytes_in_period": logs_retention_by_tier["90d"], "teams_with_logs_records_in_period": logs_records_rows, + "teams_with_web_logs_records_in_period": sdk_logs_by_suffix["web"], "teams_with_ios_logs_records_in_period": sdk_logs_by_suffix["ios"], "teams_with_react_native_logs_records_in_period": sdk_logs_by_suffix["react_native"], "teams_with_android_logs_records_in_period": sdk_logs_by_suffix["android"], "teams_with_flutter_logs_records_in_period": sdk_logs_by_suffix["flutter"], + "teams_with_ruby_logs_records_in_period": sdk_logs_by_suffix["ruby"], } @@ -2666,10 +2671,12 @@ def _get_team_report(all_data: dict[str, Any], team: Team) -> UsageReportCounter logs_retention_90d_mb_in_period=int( all_data["teams_with_logs_retention_90d_bytes_in_period"].get(team.id, 0) // 1_000_000 ), + web_logs_records_in_period=all_data["teams_with_web_logs_records_in_period"].get(team.id, 0), ios_logs_records_in_period=all_data["teams_with_ios_logs_records_in_period"].get(team.id, 0), react_native_logs_records_in_period=all_data["teams_with_react_native_logs_records_in_period"].get(team.id, 0), android_logs_records_in_period=all_data["teams_with_android_logs_records_in_period"].get(team.id, 0), flutter_logs_records_in_period=all_data["teams_with_flutter_logs_records_in_period"].get(team.id, 0), + ruby_logs_records_in_period=all_data["teams_with_ruby_logs_records_in_period"].get(team.id, 0), ) diff --git a/posthog/temporal/tests/usage_report/test_aggregate_activity.py b/posthog/temporal/tests/usage_report/test_aggregate_activity.py index c271a4dc2238..c7862285f980 100644 --- a/posthog/temporal/tests/usage_report/test_aggregate_activity.py +++ b/posthog/temporal/tests/usage_report/test_aggregate_activity.py @@ -98,10 +98,12 @@ def _canned_query_payload(query_name: str, team_a_id: int, team_b_id: int, *extr return {"count": [(team_b_id, 100)], "read_bytes": [(team_b_id, 5_000_000)]} if query_name == "sdk_logs_records": return { + "web": [(team_a_id, 3)], "ios": [(team_a_id, 4)], "react_native": [(team_b_id, 6)], "android": [], "flutter": [], + "ruby": [(team_a_id, 7)], } if query_name == "logs_retention_bytes": return { @@ -235,6 +237,11 @@ async def test_aggregate_writes_chunks_and_manifest(minio_workflow_ctx: Workflow assert by_org[str(org_b.id)]["api_queries_query_count"] == 100 assert by_org[str(org_b.id)]["api_queries_bytes_read"] == 5_000_000 + # sdk_logs_records multi-key fan-out reaches the per-SDK log counters. + assert by_org[str(org_a.id)]["web_logs_records_in_period"] == 3 + assert by_org[str(org_a.id)]["ios_logs_records_in_period"] == 4 + assert by_org[str(org_a.id)]["ruby_logs_records_in_period"] == 7 + # has_non_zero_usage is computed and present on every line. assert by_org[str(org_a.id)]["has_non_zero_usage"] is True assert by_org[str(org_b.id)]["has_non_zero_usage"] is True diff --git a/posthog/temporal/usage_report/queries.py b/posthog/temporal/usage_report/queries.py index 8421b5c1ad78..51a5e2790fad 100644 --- a/posthog/temporal/usage_report/queries.py +++ b/posthog/temporal/usage_report/queries.py @@ -469,10 +469,12 @@ class QuerySpec: fn=_sdk_logs_records, output="multi", multi_keys_mapping={ + "web": "teams_with_web_logs_records_in_period", "ios": "teams_with_ios_logs_records_in_period", "react_native": "teams_with_react_native_logs_records_in_period", "android": "teams_with_android_logs_records_in_period", "flutter": "teams_with_flutter_logs_records_in_period", + "ruby": "teams_with_ruby_logs_records_in_period", }, ), QuerySpec(