Created
May 7, 2026 16:27
-
-
Save nandosb/78393e5fda9b5925f2960adf972b114e to your computer and use it in GitHub Desktop.
# Jot Down: Campaign XRAY — CSV Download for Current Campaign
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Jot Down: Campaign XRAY — CSV Download for Current Campaign | |
| Author: mailto:fernando.serrano@yalo.com | |
| Doc Type: Jot Down | |
| Feature: Campaign XRAY — CSV Download | |
| Quarter: 2026-Q2 | |
| ## References | |
| Source: Manual input (free-text request) — "Add a button to download the information for the current campaign from the Campaign XRAY app. FE only, no new BE requests." | |
| Related Linear project: [Campaigns X-Ray](https://linear.app/yalo/project/e9ff5883-957c-46f9-b099-09ec51f9026c) | |
| ## Author / Date / Status | |
| - **Author:** [fernando.serrano@yalo.com](mailto:fernando.serrano@yalo.com) | |
| - **Date:** 2026-05-06 | |
| - **Status:** Draft | |
| --- | |
| ## 1. Architectural Decisions | |
| | Decision | Options Considered | Chosen Approach | Rationale | | |
| | --- | --- | --- | --- | | |
| | Where the button lives | Detail view only / List view only / Both | **Detail view only** (`CampaignXRayDetail.tsx`) | Matches the user's literal request ("current campaign") and the data already in scope on that screen. List-view export is out of scope. | | |
| | Export format | CSV / JSON / Both | **CSV** | Stakeholders open exports in Sheets/Excel; a single human-readable file is enough. JSON revisit if devs ask later. | | |
| | File generation library | papaparse / hand-rolled / `react-file-download` (already in deps) | **`react-file-download`** (already a dep, currently unused) for the download trigger; a small in-house CSV serializer for the string itself | No new dependency. The serializer is a ~30-line util that handles RFC-4180 quoting + UTF-8 BOM so Excel renders accents correctly. | | |
| | Activity / reasons rows source | Currently loaded only / fan-out re-fetch all pages / skip rows | **Currently loaded only** | Honors "no new BE request" — `CampaignActivityTable` already aggregates reasons client-side from `useGetCampaignDeliveryStats`. We export exactly what's on screen. | | |
| | Filename pattern | name slug + timestamp / sendId + date / campaignId only | **`campaign-xray_<campaign-name-slug>_<YYYYMMDD-HHmm>.csv`** | Human-readable, sortable in Finder/Explorer, includes campaign identity. Slugify util to be added under `helpers/`. | | |
| | Feature flag rollout | No flag / boolean flag / multi-variant flag | **Boolean flag, account-level rollout** via existing [Split.io](http://Split.io) infra | Lets us ship behind a kill-switch and roll out per-customer. Reuses the existing `useFeatureFlagTreatment` hook (`apps/studio/src/hooks/useFeatureFlagTreatment.ts`) with `useProvider: true` so the Split key is the customer/account slug. | | |
| | Split key | userEmail / customerSlug / botSlug | **customerSlug** (account-level) | The existing hook supports `userEmail` (default) or `customerSlug` via `useProvider: true`. `botSlug` is not supported by the hook today, and account-level matches how other Studio splits roll out. Recorded as the closest implementable mapping of "orgId / botSlug" given the current infra. | | |
| | Filter behavior | Export all reasons regardless of UI filters / Export only what user has filtered | **Export only currently filtered rows** | The activity table already supports stage + reason filters. "Download what you see" is the least surprising behavior and avoids an awkward "download unfiltered" mode. | | |
| | GTM coverage | None / click only / click + success/error | **Click only** — `campaign-xray_export_clicked` | Small surface, low ambiguity, easy to wire on first cut. We already track page view; we don't need outcome events for a synchronous client-side download. | | |
| --- | |
| ## 2. Overall Solution | |
| **What we're building.** A single "Download CSV" button on the Campaign XRAY detail page (`/campaigns/x-ray/:campaignId`). When the user clicks it, we serialize the in-memory campaign + delivery-stats data into a CSV file and trigger a browser download. No network calls. No backend changes. | |
| **End-to-end flow (user perspective):** | |
| 1. User opens a campaign detail page in Campaign XRAY. | |
| 2. The two existing React Query hooks (`useGetCampaign`, `useGetCampaignDeliveryStats`) load campaign metadata and delivery stages. | |
| 3. A new "Download CSV" `IconButton` (file-download icon) sits in the header area next to the existing layout/error toggles. | |
| 4. Button is disabled while either query is loading. Hidden entirely if the Split treatment is `off`. | |
| 5. Click → we build a CSV string from the cached query data + the current activity-table filter state → trigger download via `react-file-download` → push a GTM event. | |
| **End-to-end flow (data):** | |
| ``` | |
| useGetCampaign(campaignId) \ | |
| useGetCampaignDeliveryStats(sendId) → buildCampaignXRayCsv(...) → fileDownload(csv, filename) | |
| activity-table filter state (lifted up) / | |
| ``` | |
| The CSV is a single file with three logical sections separated by blank lines: | |
| - **Section A — Campaign metadata** (key/value rows): name, type, status, goal slug, audience name, channel, send type, send date, send id, created by, created at (UTC + local), notification template name, plus flattened template params, hidden params, and automation step params. | |
| - **Section B — Delivery funnel stages** (table rows): for each stage in the depth-first walk of `campaignDeliveryStats.series[0].stages`, one row per stage with `parent stage`, `stage name`, `quantity`. Walks `children[]` recursively. | |
| - **Section C — Activity (reasons)** (table rows): the same `(stage, reasonType, amount)` rows the user currently sees in `CampaignActivityTable`, after applying the active stage filter and reason search query. | |
| --- | |
| ## 3. Database Model Changes | |
| **N/A — no database changes.** This is a frontend-only feature that consumes data already loaded by existing React Query hooks. No new collections, no schema migrations. | |
| --- | |
| ## 4. API Specifications | |
| **N/A — no new or modified API endpoints.** The feature reuses the existing reads: | |
| | Hook | Endpoint (existing) | Used for | | |
| | --- | --- | --- | | |
| | `useGetCampaign({ id })` | Existing campaign detail GET (commerce-manager-bff) | Section A of the CSV | | |
| | `useGetCampaignDeliveryStats(sendId, botSlug)` | Existing delivery-stats GET (commerce-manager-bff) | Sections B and C of the CSV | | |
| No additional calls. No retries. No new query keys. | |
| **Acceptance criteria for data sourcing:** | |
| - ✅ The download produces output with zero new network requests issued at click time (verifiable via DevTools Network tab). | |
| - ✅ The data in the CSV is consistent with what is rendered on screen at the moment of click. | |
| --- | |
| ## 5. System Design | |
| **Affected services / repos:** | |
| - `frontend-platform` → `apps/studio` (the only repo touched). | |
| **Components touched / added:** | |
| ``` | |
| apps/studio/src/containers/Engagement/CampaignsError/ | |
| ├── CampaignXRayDetail.tsx (modified — add button + handler) | |
| ├── CampaignActivityTable.tsx (modified — lift filter state up via props) | |
| ├── helpers/ | |
| │ ├── buildCampaignXRayCsv.ts (new) | |
| │ ├── buildCampaignXRayCsv.test.ts (new) | |
| │ ├── slugifyCampaignName.ts (new) | |
| │ ├── slugifyCampaignName.test.ts (new) | |
| │ ├── csvSerialize.ts (new — RFC-4180 + UTF-8 BOM helper) | |
| │ └── csvSerialize.test.ts (new) | |
| ├── constants/ | |
| │ └── AnalyticsEvents.ts (modified — add CAMPAIGN_XRAY_EXPORT_CLICKED) | |
| └── (no new components — the button is an `IconButton` inline) | |
| ``` | |
| New Split entry: | |
| - `apps/studio/src/constants/SplitNames.ts` → add `CAMPAIGN_XRAY_CSV_EXPORT = 'campaign_xray_csv_export'`. | |
| **Sequence of operations (click handler):** | |
| 1. Read `campaign` and `campaignDeliveryStats` from the already-mounted React Query hooks (passed as props/refs from `CampaignXRayDetail`). | |
| 2. Read the activity-table filter state (`selectedStage`, `searchQuery`) — lifted up to `CampaignXRayDetail` so the click handler can see it. | |
| 3. Call `buildCampaignXRayCsv({ campaign, deliveryStats, activityFilters, locale })` → returns `{ csv: string, filename: string }`. | |
| 4. Call `fileDownload(csv, filename, 'text/csv;charset=utf-8')` from `react-file-download` (the lib accepts a string + filename + MIME). | |
| 5. `Analytics.pushEvent({ event: 'campaign-xray_export_clicked', campaignId, sendId, customerSlug, botSlug, author, timestamp })` via the existing `useAnalytics` hook in `apps/studio/src/containers/Engagement/hooks/useAnalytics.tsx`. | |
| No state machine, no async work, no error path beyond a try/catch that surfaces a Snackbar/Toast on serialization failure (extremely unlikely — defensive only). | |
| --- | |
| ## 6. Service Interactions | |
| **N/A — no inter-service communication is added.** The feature is entirely client-side; no new messages, queues, or events leave the browser. | |
| --- | |
| ## 7. Dependencies | |
| - **`react-file-download@0.3.5`** — already present in `apps/studio/package.json` (line ~208), currently unused. Provides `fileDownload(data, filename, mimeType)`. No version bump. | |
| - **`@yalo/commons` Analytics** — existing. | |
| - **`@splitsoftware/splitio-redux`** — existing, used through `useFeatureFlagTreatment`. | |
| No new dependencies, no version bumps, no shared library updates. | |
| --- | |
| ## 8. Feature Flag ([Split.io](http://Split.io)) | |
| | Field | Value | | |
| | --- | --- | | |
| | **Split name** | `campaign_xray_csv_export` | | |
| | **Treatments** | `on`, `off` | | |
| | **Default treatment** | `off` | | |
| | **Service / Repo** | `frontend-platform` → `apps/studio` | | |
| | **Evaluation point** | `CampaignXRayDetail.tsx` — at the top of the component, via `useFeatureFlagTreatment(SplitNames.CAMPAIGN_XRAY_CSV_EXPORT, { useProvider: true })`. The button is conditionally rendered when the treatment is `'on'`. | | |
| | **Key** | Customer (account) slug — `customers.customerSelected.slug`, supplied by the existing hook when `useProvider: true` | | |
| | **Treatment mapping** | `'on'` → render the Download CSV `IconButton`. `'off'` (and any other value) → do not render the button. | | |
| | **Fallback behavior** | If the Split SDK can't reach Split (timeout, offline, init not complete), `selectTreatmentValue` returns `'control'` / `undefined`. Code MUST treat anything other than `'on'` as `off` — i.e. button hidden. This matches the safe / current behavior (no button today). | | |
| **Constants to add** in `apps/studio/src/constants/SplitNames.ts`: | |
| ```tsx | |
| CAMPAIGN_XRAY_CSV_EXPORT = 'campaign_xray_csv_export', | |
| ``` | |
| [**Split.io](http://Split.io) platform setup (manual, by whoever owns Split):** | |
| - Create the split with treatments `on` / `off`, default `off`. | |
| - Allowlist target customer slugs as the rollout proceeds. | |
| --- | |
| ## 9. Deployment Stages | |
| | Stage | What ships | Risks | Mitigation | | |
| | --- | --- | --- | --- | | |
| | 1. Code merge with flag `off` | All code (button + helpers + tests + Split entry) merges to `main` and ships to staging/prod with the split defaulting to `off` everywhere | Code that's deployed but unreachable still introduces bundle weight | The new helpers add < 5 KB gzipped; CSV string is built lazily on click, not on render | | |
| | 2. Internal QA | Allowlist Yalo's internal customer slug(s) on Split → treatment `on` for the QA team only | QA finds a regression | Roll back is just flipping the treatment to `off` for the allowlisted slug — no redeploy | | |
| | 3. Pilot | Allowlist 1–3 friendly customer slugs | Pilot customer hits an unexpected data shape (e.g. a campaign with 5 000 reason rows produces a multi-MB file) | Built-in: file size is bounded by what the page already renders. If a complaint comes in, flip the slug back to `off`. | | |
| | 4. General availability | Set the split's default to `on` | None expected | The flag remains in code so we keep a kill switch; remove the flag in a follow-up PR after one full sprint of GA stability | | |
| **Rollback plan (any stage):** flip the Split treatment back to `off` for the affected key(s). No code revert required. No data state to clean up. | |
| --- | |
| ## 10. Testing Strategy | |
| ### Unit tests (Jest + RTL, following `.claude/skills/test-patterns.md` via `test-patterns-enforcer`) | |
| **`csvSerialize.test.ts`** — pure function tests: | |
| - Quotes any field that contains `,`, `"`, `\n`, or `\r`. | |
| - Escapes embedded `"` by doubling. | |
| - Prepends UTF-8 BOM (``) so Excel renders Spanish/Portuguese accents correctly. | |
| - Joins rows with `\r\n` per RFC-4180. | |
| - Empty input produces a single trailing newline (no crash). | |
| **`slugifyCampaignName.test.ts`**: | |
| - Lowercases, replaces whitespace with `-`, strips diacritics, drops non-`[a-z0-9-]` characters. | |
| - Truncates to 60 chars. | |
| - Empty / undefined input returns `'campaign'`. | |
| **`buildCampaignXRayCsv.test.ts`**: | |
| - Returns the expected three-section CSV string for a fixture from `constants/campaignDataMock.ts` + `constants/campaignDeliveryStatsMock.ts` (already in repo). | |
| - Honors the `activityFilters.selectedStage` parameter (rows for other stages excluded). | |
| - Honors the `activityFilters.searchQuery` parameter (case-insensitive substring on `reasonType`). | |
| - Filename matches `^campaign-xray_[a-z0-9-]+_\d{8}-\d{4}\.csv$`. | |
| - Walks nested `stages[].children[]` depth-first (verify with a fixture that has nested children). | |
| - Renders `notificationTemplateParams`, `notificationTemplateHiddenParams`, `automationStepParams` as flattened key/value rows. | |
| - Treats missing optional fields as empty strings, never `undefined`. | |
| **`CampaignXRayDetail.test.tsx`** (new tests on the modified file): | |
| - When Split treatment is `'on'` → button renders. | |
| - When Split treatment is `'off'` / `undefined` / `'control'` → button does not render. | |
| - Click while either query is loading is a no-op (button is `disabled`). | |
| - Click invokes `fileDownload` once with the expected MIME and a non-empty CSV string. Mock `react-file-download` per file. | |
| - Click pushes the `campaign-xray_export_clicked` analytics event with `campaignId`, `sendId`, `customerSlug`, `botSlug`, `author` populated. Mock `@yalo/commons` per the standing exception (see `.claude/CLAUDE.md` § Analytics). | |
| **`CampaignActivityTable.test.tsx`** (regression on the lifted-state refactor): | |
| - Existing filter and search behavior still work when state lives one level up. | |
| - Existing copy-to-clipboard behavior still works. | |
| ### Integration / e2e | |
| - Manual smoke in staging: open a campaign with at least one reason row, click the button, verify the CSV opens cleanly in Sheets and Excel (BOM check), verify the filename matches the pattern, verify accents render. | |
| - DevTools Network tab: zero new requests at click time. | |
| ### Acceptance criteria — testable conditions for "done" | |
| 1. A new "Download CSV" `IconButton` (file-download icon, with tooltip `Download CSV`) is visible on the Campaign XRAY detail page header **only when** Split `campaign_xray_csv_export` evaluates to `'on'` for the current customer slug. | |
| 2. Clicking the button while data is still loading does nothing (button is `disabled`). | |
| 3. Clicking the button after data has loaded triggers a browser download whose filename matches `campaign-xray_<slug>_<YYYYMMDD-HHmm>.csv`. | |
| 4. The downloaded file opens in Google Sheets and in Microsoft Excel without character corruption (UTF-8 BOM verified). | |
| 5. The CSV contains three logical sections — campaign metadata, delivery funnel stages (depth-first), and activity reasons — exactly matching what is on screen given the user's current activity-table filters. | |
| 6. Zero new network calls are issued by the click (verified via Network tab). | |
| 7. A `campaign-xray_export_clicked` analytics event is pushed once per click with `campaignId`, `sendId`, `customerSlug`, `botSlug`, `author`, `timestamp`. | |
| 8. With Split treatment `'off'` / `'control'` / `undefined`, the button is **not rendered** (DOM-absent, not just hidden). | |
| 9. Lint, typecheck, unit tests pass on `yarn workspace @yalo/studio lint` / `test:ci` / `build`. Coverage on touched files does not drop. | |
| 10. PR stays under 500 lines (CI warn threshold) and well under 1500 (CI fail threshold) — see `.claude/CLAUDE.md` § PR Size. | |
| --- | |
| ## 11. Frontend Components | |
| ### Component inventory | |
| | Component | File | Change | Notes | | |
| | --- | --- | --- | --- | | |
| | `CampaignXRayDetail` | `apps/studio/src/containers/Engagement/CampaignsError/CampaignXRayDetail.tsx` | **Modified** | Add Split-flag check, add Download CSV `IconButton` to the header `Stack` (line ~295), wire `onClick` handler, lift `selectedStage` • `searchQuery` from `CampaignActivityTable` so the handler can read them. | | |
| | `CampaignActivityTable` | `apps/studio/src/containers/Engagement/CampaignsError/CampaignActivityTable.tsx` | **Modified (refactor)** | Accept `selectedStage`, `onSelectedStageChange`, `searchQuery`, `onSearchQueryChange` as props. Existing copy-to-clipboard behavior remains unchanged. | | |
| | `buildCampaignXRayCsv` | `apps/studio/src/containers/Engagement/CampaignsError/helpers/buildCampaignXRayCsv.ts` | **New** | Pure function. Signature: `(args: { campaign: ICampaign; deliveryStats: ICampaignStatsChart; activityFilters: { selectedStage: string; searchQuery: string }; locale: string }) => { csv: string; filename: string }`. | | |
| | `slugifyCampaignName` | `helpers/slugifyCampaignName.ts` | **New** | `(name: string \ | | |
| | `csvSerialize` | `helpers/csvSerialize.ts` | **New** | `(rows: string[][]) => string` — RFC-4180 + UTF-8 BOM. | | |
| | `ANALYTICS_EVENTS` | `constants/AnalyticsEvents.ts` | **Modified** | Add `CAMPAIGN_XRAY_EXPORT_CLICKED: 'campaign-xray_export_clicked'`. | | |
| | `SplitNames` | `apps/studio/src/constants/SplitNames.ts` | **Modified** | Add `CAMPAIGN_XRAY_CSV_EXPORT = 'campaign_xray_csv_export'`. | | |
| ### Layout / placement | |
| The button slots into the existing header `Stack` of `CampaignXRayDetail.tsx` (around lines 286–321 in the current file), to the right of the layout/error-caption toggles: | |
| ``` | |
| [campaign name + channel chip] [Error captions toggle] [Layout toggle] [⬇ Download CSV] | |
| ``` | |
| Use `IconButton` from `@engyalo/design-system` (consistent with `CampaignActivityTable`'s clipboard button), `Tooltip` for the label, FontAwesome `fa-solid fa-download` icon class (consistent with the existing `fa-solid fa-copy` / `fa-compress` / `fa-expand` usage in this file). | |
| Disable when either `isLoading` or `isCampaignDeliveryStatsLoading` is true. Hide entirely when the Split treatment is not `'on'`. | |
| ### Payload shapes (already loaded — no new requests) | |
| ```tsx | |
| // from useGetCampaign | |
| interface ICampaign { | |
| name: string; | |
| type: string; | |
| status: string; | |
| goal?: { slug: string }; | |
| audience?: { name: string }; | |
| sendRule?: { scheduleData?: { date?: string } }; | |
| notificationTemplate?: { name: string }; | |
| summary?: Array<{ id: string }>; // sendId = summary[0].id | |
| createdBy: string; | |
| createdAt: string; // ISO | |
| notificationTemplateParams?: Array<{ key: string; value: string }>; | |
| notificationTemplateHiddenParams?: Array<{ key: string; value: string }>; | |
| automationStepParams?: Array<{ key: string; value: string }>; | |
| } | |
| // from useGetCampaignDeliveryStats — see types/CampaignsBff/CampaignDeliveryStats.ts | |
| interface ICampaignStatsChart { | |
| campaignName: string; | |
| sendType: 'single-send' | 'recurring'; | |
| campaignType: 'manual' | 'intelligent'; | |
| channel: string; | |
| series: Array<{ | |
| date: string; | |
| stages: IStage[]; | |
| }>; | |
| } | |
| interface IStage { | |
| name: string; | |
| quantity: number; | |
| details: { reasons: Array<{ type: string; amount: number }> }; | |
| children: IStage[]; | |
| } | |
| ``` | |
| ### Integration points with backend APIs | |
| None added. The feature is read-only against the existing React Query cache populated by `useGetCampaign` and `useGetCampaignDeliveryStats`. See Section 4. | |
| ### CSV layout (concrete spec for `buildCampaignXRayCsv`) | |
| ``` | |
| Campaign | |
| Field,Value | |
| Name,<campaign.name> | |
| Type,<campaign.type> | |
| Status,<campaign.status> | |
| Goal,<campaign.goal.slug> | |
| Audience,<campaign.audience.name> | |
| Channel,<deliveryStats.channel> | |
| Send type,<deliveryStats.sendType> | |
| Send date,<campaign.sendRule.scheduleData.date> | |
| Send ID,<campaign.summary[0].id> | |
| Created by,<campaign.createdBy> | |
| Created at (UTC),<campaign.createdAt> | |
| Created at (local),<moment.utc(campaign.createdAt).local().format('LLLL')> | |
| Notification template,<campaign.notificationTemplate.name> | |
| Template param: <key>,<value> ← repeated per notificationTemplateParams entry | |
| Hidden param: <key>,<value> ← repeated per notificationTemplateHiddenParams entry | |
| Automation step param: <key>,<value> ← repeated per automationStepParams entry | |
| <blank line> | |
| Delivery funnel | |
| Parent stage,Stage,Quantity | |
| <parent or empty>,<stage.name>,<stage.quantity> | |
| …depth-first walk of stages[] and children[]… | |
| <blank line> | |
| Activity (reasons) — filtered | |
| Stage,Reason,Amount | |
| <row.stage>,<row.reasonType>,<row.amount> | |
| …filtered by activityFilters.selectedStage and activityFilters.searchQuery… | |
| ``` | |
| ### GTM Events | |
| | Event Name | Trigger | | |
| | --- | --- | | |
| | `campaign-xray_export_clicked` | User clicks the Download CSV button on `CampaignXRayDetail`. Payload includes `campaignId`, `sendId`, `customerSlug`, `botSlug`, `author`, `timestamp`. Pushed via the existing `useAnalytics().trackEvent(...)` hook. | | |
| --- | |
| ## Risks & Mitigations | |
| | Risk | Likelihood | Impact | Mitigation | | |
| | --- | --- | --- | --- | | |
| | Campaigns with very large reason lists produce multi-MB CSVs | Low | Slow browser pause during serialization | Serializer is O(n) and runs on click only. The cap is whatever fits in the existing in-memory React Query cache — i.e. the page already loads it. | | |
| | Excel mis-renders accents | Medium if BOM is forgotten | Stakeholders complain | UTF-8 BOM is part of the serializer with a unit test asserting its presence. | | |
| | Template params contain newlines or commas | Medium | Broken CSV row | Serializer quotes fields containing `"`, `,`, `\r`, `\n`. Unit-tested. | | |
| | Split SDK init race makes the button briefly invisible after page load | Low | Cosmetic flash | `useFeatureFlagTreatment` returns `undefined` during init; we treat that as `off` and render nothing — same outcome as flag-off, no flash. | | |
| | Activity filters change between read and click | Low | CSV mismatches the on-screen view | Filter state is read inside the click handler synchronously, so the CSV always reflects the table state at the moment of click. | | |
| --- | |
| ## Out of Scope | |
| - Any backend endpoint additions or modifications. | |
| - Exporting the campaign **list** (`CampaignsXRayViewer.tsx`). | |
| - Exporting per-recipient activity rows (the activity table aggregates reasons, not individual users; per-recipient export would require BE work). | |
| - JSON / Excel-binary / PDF export formats. | |
| - Re-fetching paginated stages or reasons that aren't already on screen. | |
| - Email-the-CSV-to-yourself flows (covered separately under Linear MKT-335). | |
| - A reusable `<DownloadCSVButton>` in `commerce-ui-components` (defer until a second consumer appears). | |
| --- | |
| ## Notes for the implementing agent | |
| - **Worktree first**: per `.claude/CLAUDE.md` (frontend-platform) § Worktree Isolation, run `yarn wt-add <branch>` before editing. Never edit in the main directory. | |
| - **Routing**: this page already uses `@reach/router`. Do not introduce `useParams` from `react-router-dom`. | |
| - **Analytics**: use the existing `useAnalytics` hook (`apps/studio/src/containers/Engagement/hooks/useAnalytics.tsx`) — its `trackEvent` already enriches with `accountId`, `botSlug`, `author`. You only need to add the event-name constant in `constants/AnalyticsEvents.ts` and call `trackEvent(ANALYTICS_EVENTS.CAMPAIGN_XRAY_EXPORT_CLICKED, { campaignId, sendId, timestamp })`. | |
| - **TypeScript / test patterns**: per `.claude/CLAUDE.md`, delegate to `typescript-patterns-enforcer` and `test-patterns-enforcer` subagents before writing the new files. | |
| - **PR size budget**: estimated ~250–350 lines including tests. Comfortably under the 500-line CI warn threshold. | |
| - **Linear ticket title**: `feat(studio): add CSV export to Campaign XRAY detail — <TICKET-ID>`. Create the ticket in Linear's Commerce team if one doesn't exist; CI requires `[A-Z]+-\d+` in the PR title. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment