From 0f5e98fffe762a8a1cdd342490ebd20663e3230c Mon Sep 17 00:00:00 2001 From: Sahil Gupta Date: Fri, 17 Jul 2026 17:41:19 +0530 Subject: [PATCH 1/2] docs: Interceptor Usage-Reports track design + stacked-PR base (RQ-3644 Task 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design spec for the Interceptor analytics instrumentation that lets BrowserStack Usage Reports attribute Interceptor usage per group/user. Seeds the stacked-PR base branch for subtasks 3.1–3.8 (RQ-4675–4682). Co-Authored-By: Claude Opus 4.8 (1M context) --- ...-07-17-interceptor-usage-reports-design.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-17-interceptor-usage-reports-design.md diff --git a/docs/superpowers/specs/2026-07-17-interceptor-usage-reports-design.md b/docs/superpowers/specs/2026-07-17-interceptor-usage-reports-design.md new file mode 100644 index 0000000000..1e1799a7de --- /dev/null +++ b/docs/superpowers/specs/2026-07-17-interceptor-usage-reports-design.md @@ -0,0 +1,57 @@ +# Interceptor → BrowserStack Usage Reports instrumentation (RQ-3644, Task 3) + +**Status:** design approved · **Parent:** [RQ-3644](https://browserstack.atlassian.net/browse/RQ-3644) (Usage & Reporting Analytics, customer AON) · **Track:** Task 3 (Interceptor); the sibling API Client track is Task 2 (separate repo, `requestly-api-client`). + +This branch (`feat/rq-3644-usage-reports`) is the **stacked-PR base** for the Interceptor track. Each `3.x` subtask lands as its own PR targeting this branch and merges *down* into it; this branch merges to `master` last. + +--- + +## Goal + +Let BrowserStack org admins see Requestly **Interceptor** adoption/activity in enterprise **Usage Reports → Team & User Reports**. Interceptor usage already flows to `live_plus.requestly_events_partitioned` via EDS, and every event is auto-enriched at a single choke-point — but it can't be attributed to a BrowserStack **group** because the group id never reaches the event. + +## Attribution model — group is a property of the USER, not the workspace + +The audit (RQ-4674) originally assumed the group could come from the active workspace's `browserstackDetails.groupId`. **That is wrong for attribution:** in Interceptor a workspace/team is independent of the user's BrowserStack group. The canonical definition (api-server `/api/v1/me`, ADR-196) is: a user's `group_id` = the `bsGroupId` on their BrowserStack IAAM user-mapping — present when the user is BS-linked, `null` otherwise ("auth not merged"). `sub_group_id` (the team/subgroup) is **not consumed by Interceptor** right now. + +### How the user→group link exists in Interceptor today + +Interceptor has no `/me` and no `user_mappings` table (it runs on requestly-cloud/Firebase). The user's BS group reaches the frontend through exactly one correct channel: the **billing team**. + +- requestly-cloud maps a BS group to a billing team **1:1 and deterministically**: `getBillingTeamIdFromGroupId(groupId)` → `` `browserstack-${groupId}` `` (`browserstack/utils/index.ts:12`). That billing-team doc carries `browserstackGroupId = ` (`billing/types.ts:28`, a **string**). +- On BS user sync (`browserstack/services/userSync.ts` `processGroup`), the user is added to `browserstack-`; when their BS org changes they're **removed** from the old one → a synced user is a member of **exactly one** `browserstack-*` billing team. +- Interceptor loads the user's billing teams globally via `useBillingTeamsListener` (`app/src/hooks`/mounted in `AppLayout:47`) — a Firestore listener on `billing` where `members.{uid} != null` (plus company-domain-owned teams). So `browserstackGroupId` is available in the store for every event. + +**The other two things that look like a group source but aren't the right one:** +- Workspace `browserstackDetails.groupId` (`features/workspaces/types/index.ts:25`) — workspace/team-level, the semantic we explicitly rejected. +- `user.details.organization` (`AuthHandler.ts:90`) — from `getEnterpriseAdminDetails`, only `{ workspaces: [{ adminName, adminEmail, workspaceName }] }` matched by email domain (admin-only). **No group id.** + +## 3.1 (RQ-4675) — plumb `group_id` onto every event + +1. **Selector** `getUserBrowserstackGroupId` (new, in `store/features/billing/selectors.ts`): from `getAvailableBillingTeams` + the signed-in `uid`, pick the team where `uid ∈ members` **and** `browserstackGroupId` is set (prefer member-of over domain-matched teams). Return that `browserstackGroupId` or `null`. Consolidates the `uid in team.members` filter currently copy-pasted in ~6 components. +2. **Global setter**: set `window.currentlyActiveBrowserstackGroupId` from that selector when billing teams / user settle (mirrors the existing `window.currentlyActive*` globals that `trackEvent` reads). +3. **Choke-point** (`app/src/modules/analytics/index.js`, the `trackEvent` enrichment block): `newParams.group_id = window.currentlyActiveBrowserstackGroupId ?? null;` +4. **`sub_group_id`**: intentionally **not** emitted — no sub-group source in Interceptor billing. Leave a comment marking it future-consumable if Team-level (sub_group) reports are ever needed. + +**Field name:** `group_id` (snake_case) — matches the BigQuery sink and the API Client envelope, so the cross-product query (`JSON_VALUE(event_details,'$.group_id')`) is uniform. RQ stores the id as a string; the API Client emits a number; `JSON_VALUE` stringifies both identically, so emitting the string as-is is warehouse-uniform. + +**Null behavior:** users with no BS-linked billing team (email/Firebase-only, or unlinked) → `group_id = null`, matching the canonical `/me` behavior. Personal-workspace events still carry the user's `group_id` (the group is user-linked, not workspace-linked) — the personal-workspace *reporting* decision is 3.8. + +## Verification + +Replicate the API Client's `_qa` marker rig (RQ-4684 / PR #3482 in `requestly-api-client`): an **env-gated** marker stamped into the event payload so locally-driven test events are filterable in BigQuery and excludable from customer reports (`AND JSON_VALUE(event_details,'$._qa') IS NULL`). The terminal EDS→BigQuery sink is not observable from this repo (PostHog integration commented out, `local` is a no-op, only GA gtag live) — confirming/documenting it is **3.6 (RQ-4680)**, on which reliable end-to-end verification of 3.1/3.3/3.5 depends. + +## Stacked-PR plan (base → `master`) + +| Subtask | Ticket | What | +| --- | --- | --- | +| 3.1 | RQ-4675 | `group_id` onto every event (this design) — **first stacked PR** | +| 3.2 | RQ-4676 | `sharedList_updated` event | +| 3.3 | RQ-4677 | explicit `product_name = requestly_interceptor` | +| 3.4 | RQ-4678 | reconcile extension-popup `rule_toggled` payload | +| 3.5 | RQ-4679 | `super_user_id` staff-impersonation flag | +| 3.6 | RQ-4680 | confirm & document the EDS→BigQuery sink | +| 3.7 | RQ-4681 | `browserstack_user_id` coverage (Firebase-uid → BS-user fallback) | +| 3.8 | RQ-4682 | personal-workspace (`workspaceId=null`) attribution decision | + +3.1/3.3/3.5 all edit the `analytics/index.js` enrichment block, so subtasks stack **linearly** (each branches off the previous, merges down into this base in turn) to avoid conflicts. From 9516f0d024b64f40678cba13358f05860212848a Mon Sep 17 00:00:00 2001 From: Sahil Gupta Date: Mon, 20 Jul 2026 12:59:53 +0530 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20correct=20group=5Fid=20flow=20?= =?UTF-8?q?=E2=80=94=20top-level=20EDS=20field=20via=20webapp=20overlay=20?= =?UTF-8?q?(RQ-4675)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit group_id flows the same way as browserstack_user_id: a top-level data.* field emitted in the requestly-cloud webapp eds.ts overlay (which overwrites this repo's modules/analytics via patch.sh), NOT in event_details. This repo only exposes window.currentlyActiveBrowserstackGroupId (selector + hook); the emit half is requestly-cloud PR #861. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...26-07-17-interceptor-usage-reports-design.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/superpowers/specs/2026-07-17-interceptor-usage-reports-design.md b/docs/superpowers/specs/2026-07-17-interceptor-usage-reports-design.md index 1e1799a7de..c2477a0cc8 100644 --- a/docs/superpowers/specs/2026-07-17-interceptor-usage-reports-design.md +++ b/docs/superpowers/specs/2026-07-17-interceptor-usage-reports-design.md @@ -28,12 +28,17 @@ Interceptor has no `/me` and no `user_mappings` table (it runs on requestly-clou ## 3.1 (RQ-4675) — plumb `group_id` onto every event -1. **Selector** `getUserBrowserstackGroupId` (new, in `store/features/billing/selectors.ts`): from `getAvailableBillingTeams` + the signed-in `uid`, pick the team where `uid ∈ members` **and** `browserstackGroupId` is set (prefer member-of over domain-matched teams). Return that `browserstackGroupId` or `null`. Consolidates the `uid in team.members` filter currently copy-pasted in ~6 components. -2. **Global setter**: set `window.currentlyActiveBrowserstackGroupId` from that selector when billing teams / user settle (mirrors the existing `window.currentlyActive*` globals that `trackEvent` reads). -3. **Choke-point** (`app/src/modules/analytics/index.js`, the `trackEvent` enrichment block): `newParams.group_id = window.currentlyActiveBrowserstackGroupId ?? null;` -4. **`sub_group_id`**: intentionally **not** emitted — no sub-group source in Interceptor billing. Leave a comment marking it future-consumable if Team-level (sub_group) reports are ever needed. +**The patch model is the key constraint.** The production web app = this interceptor base + the `requestly-cloud` `webapp` overlay patched on top, and `patch.sh` **overwrites** `modules/analytics/` wholesale (`cp -r webapp/src/modules/analytics/. → build/app/...`). The real EDS emission lives in the overlay (`webapp/src/modules/analytics/integrations/eds.ts`), which builds a `POST /send_event` envelope with `browserstack_user_id` as a **top-level `data.*` field** (not inside `event_details`). So `group_id` must flow the **same way as `browserstack_user_id`** — a top-level `data.group_id` field emitted in `eds.ts`, in `requestly-cloud`, not this repo. This repo only exposes the value. -**Field name:** `group_id` (snake_case) — matches the BigQuery sink and the API Client envelope, so the cross-product query (`JSON_VALUE(event_details,'$.group_id')`) is uniform. RQ stores the id as a string; the API Client emits a number; `JSON_VALUE` stringifies both identically, so emitting the string as-is is warehouse-uniform. +Split across the two repos: + +1. **Interceptor (this repo — survives the patch):** + - **Selector** `getUserBrowserstackGroupId` (new, `store/features/billing/selectors.ts`): from `getAvailableBillingTeams` + the signed-in `uid`, pick the team where `uid ∈ members` **and** `browserstackGroupId` is set (prefer member-of over domain-matched teams); return that `browserstackGroupId` or `null`. Consolidates the `uid in team.members` filter copy-pasted in ~6 components. + - **Hook** `useBrowserstackGroupId` (mounted in `AppLayout`): sets `window.currentlyActiveBrowserstackGroupId` from that selector — the interceptor-side source, analogous to how the auth handler exposes `browserstackId`. Does **not** touch `modules/analytics/`. +2. **requestly-cloud (`webapp` overlay):** in `integrations/eds.ts`, add `group_id: window.currentlyActiveBrowserstackGroupId ?? null` to the `data` object, a sibling of `browserstack_user_id`. +3. **`sub_group_id`**: intentionally **not** emitted — no sub-group source in Interceptor billing. Commented in `eds.ts` as future-consumable if Team-level (sub_group) reports are ever needed. + +**Field name / shape:** `group_id` (snake_case), a **top-level `data.*` field** (queried like `browserstack_user_id`, i.e. `e.group_id`, not `JSON_VALUE(event_details,'$.group_id')`). RQ stores the id as a string; the API Client emits a number — for BigQuery this is uniform. **Null behavior:** users with no BS-linked billing team (email/Firebase-only, or unlinked) → `group_id = null`, matching the canonical `/me` behavior. Personal-workspace events still carry the user's `group_id` (the group is user-linked, not workspace-linked) — the personal-workspace *reporting* decision is 3.8. @@ -54,4 +59,4 @@ Replicate the API Client's `_qa` marker rig (RQ-4684 / PR #3482 in `requestly-ap | 3.7 | RQ-4681 | `browserstack_user_id` coverage (Firebase-uid → BS-user fallback) | | 3.8 | RQ-4682 | personal-workspace (`workspaceId=null`) attribution decision | -3.1/3.3/3.5 all edit the `analytics/index.js` enrichment block, so subtasks stack **linearly** (each branches off the previous, merges down into this base in turn) to avoid conflicts. +Interceptor-side subtasks stack **linearly** on this base (each branches off the previous, merges down in turn). Note the emission half of 3.1/3.3 (`group_id`, `product_name`) and 3.5 (`super_user_id`) lands in the `requestly-cloud` `webapp` overlay (`eds.ts`), **not** this repo's `analytics/index.js` — those touch the same overlay file, so their `requestly-cloud` PRs must be coordinated there too. 3.1's emission half = `requestly-cloud` PR #861.