feat-export-timeline#35
Conversation
Implements export mode and dedicated controls for exporting event timelines across the panel components. This includes updating `Header`, `ActionButtons`, and state management in `Panel` to handle the export trigger and display accordingly. The actual export functionallity will come later
There was a problem hiding this comment.
Pull request overview
Adds an “Export Events” workflow to the analytics panel, enabling users to enter an export mode, select timeline events (including shift-click range selection), and export either a “smart” Markdown summary or raw JSON, with persisted section-toggle preferences.
Changes:
- Introduces export formatting utilities (
exportFormatter.ts) and re-exports them viasrc/lib/index.ts. - Adds export-mode UI + selection state in
Panel.tsx, plus newExportToolbar/ExportModalcomponents and selection affordances in the event list/rows. - Persists export section toggle preferences in
configStore(including a storage version bump + migration).
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/stores/configStore.ts | Adds persisted exportSections preference, setter, selector, and migration/version bump. |
| src/pages/panel/Panel.tsx | Implements export mode state, selection logic (incl. shift-range), and wiring to toolbar/modal. |
| src/pages/panel/components/index.ts | Re-exports new export components. |
| src/pages/panel/components/Header.tsx | Threads export props through header to action buttons. |
| src/pages/panel/components/ExportToolbar.tsx | New toolbar for select all/deselect/export/cancel. |
| src/pages/panel/components/ExportModalSectionToggle.tsx | New section toggle row component for smart export configuration. |
| src/pages/panel/components/ExportModal.tsx | New modal for smart/raw export, section toggles, and copy-to-clipboard behavior. |
| src/pages/panel/components/EventRowHeader.tsx | Adds export-mode checkbox + keyboard/mouse handling for selection. |
| src/pages/panel/components/EventRow.tsx | Adds export-mode styling and passes selection props through. |
| src/pages/panel/components/EventList/EventList.tsx | Threads export-mode props and selection state into rows/sticky header. |
| src/pages/panel/components/ActionButtons.tsx | Adds export button to the action bar with export-mode styling. |
| src/lib/index.ts | Re-exports export formatter APIs/types/defaults. |
| src/lib/exportFormatter.ts | New formatter utilities for Markdown “smart export” and raw JSON export. |
| package.json | Bumps package version to 1.1.0. |
Comments suppressed due to low confidence (1)
src/stores/configStore.ts:293
useConfigStore.reset()is documented as resetting user-visible settings, but it currently does not reset the newly addedexportSectionspreference. This means users can’t return export section toggles to defaults via the existing reset path. Consider includingexportSections: defaultConfig.exportSectionsin the reset state (or add a dedicated reset action if that’s the intended UX).
setExportSections: (sections) => set({ exportSections: sections }),
// Reset only user-visible settings, preserve pinned properties (internal state)
reset: () =>
set((state) => ({
maxEvents: defaultConfig.maxEvents,
theme: defaultConfig.theme,
preferredEventDetailView: defaultConfig.preferredEventDetailView,
// Preserve pinnedProperties - they're internal state not shown in Options
pinnedProperties: state.pinnedProperties,
})),
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/stores/configStore.ts:293
reset()does not reset the newly addedexportSectionspreference (it only setsmaxEvents,theme, andpreferredEventDetailView). Since the Options UI describes this action as “Reset all settings to default values”, users who changed export section toggles will keep their customexportSectionsafter reset. Consider includingexportSections: defaultConfig.exportSections(orDEFAULT_EXPORT_SECTIONS) in the reset payload (and, if intended, other user-visible settings likesectionDefaults/dismissedOnboardingModals).
setExportSections: (sections) => set({ exportSections: sections }),
// Reset only user-visible settings, preserve pinned properties (internal state)
reset: () =>
set((state) => ({
maxEvents: defaultConfig.maxEvents,
theme: defaultConfig.theme,
preferredEventDetailView: defaultConfig.preferredEventDetailView,
// Preserve pinnedProperties - they're internal state not shown in Options
pinnedProperties: state.pinnedProperties,
})),
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…er a user has seen the onboarding modal
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { cn } from '@src/lib'; | ||
|
|
||
| import { Label, Switch } from '@components/ui'; | ||
|
|
There was a problem hiding this comment.
ExportModalSectionToggle imports Label/Switch from @components/ui, but the surrounding codebase consistently imports UI primitives from @src/components/ui/* (e.g. src/pages/panel/components/detail/SectionDefaultsModal.tsx:10-11). Using a different alias just in this file is inconsistent and makes imports harder to grep/refactor; consider switching this import to the same @src/components/ui/... pattern used elsewhere.
| import { cn } from '@src/lib'; | |
| import { Label, Switch } from '@components/ui'; | |
| import { Label } from '@src/components/ui/label'; | |
| import { Switch } from '@src/components/ui/switch'; | |
| import { cn } from '@src/lib'; |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (2)
📝 WalkthroughWalkthroughAdds timeline export (Markdown/JSON) with selectable events and configurable sections, a feature-badge hook, store fields/actions (with migrations) for export sections and acknowledged features, export UI components and panel selection flow, and refactors timeout usage to use refs with unmount cleanup. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Header
participant ActionButtons
participant Panel
participant EventList
participant ExportModal
participant ConfigStore
participant Clipboard
User->>Header: click Export
Header->>ActionButtons: onExport()
ActionButtons->>ConfigStore: acknowledgeFeature('export')
ActionButtons->>Panel: enable export mode
Panel->>EventList: render with checkboxes
User->>EventList: select events (click / shift-click)
EventList->>Panel: onToggleSelect(eventId, shiftKey)
User->>ActionButtons: click Export (selected)
ActionButtons->>Panel: open ExportModal
User->>ExportModal: configure sections
ExportModal->>ConfigStore: setExportSections(...)
User->>ExportModal: click Copy
ExportModal->>ExportModal: format events (Markdown/JSON)
ExportModal->>Clipboard: copy formatted text
Clipboard-->>User: copy success
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai help |
ChatThere are 3 ways to chat with CodeRabbit:
CodeRabbit commands
Other keywords and placeholders
CodeRabbit configuration file (
|
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/stores/configStore.ts (1)
427-494:⚠️ Potential issue | 🔴 CriticalChain these migrations instead of returning from the first match.
Line 481 returns as soon as
version < 8, so installs upgrading from v7 to v9 never getacknowledgedFeatures. Older installs miss later fields for the same reason.isFeatureAcknowledged()then reads.includes()fromundefined, which can crash the panel for persisted users.🐛 Fix direction
migrate: (persistedState, version) => { - const state = persistedState as ExtensionConfig; + let state = persistedState as ExtensionConfig; // apply each migration cumulatively - if (version < 8) { - return { - ...state, - exportSections: DEFAULT_EXPORT_SECTIONS, - }; - } - if (version < 9) { - return { - ...state, - acknowledgedFeatures: defaultConfig.acknowledgedFeatures, - }; - } + if (version < 8) { + state = { + ...state, + exportSections: DEFAULT_EXPORT_SECTIONS, + }; + } + if (version < 9) { + state = { + ...state, + acknowledgedFeatures: defaultConfig.acknowledgedFeatures, + }; + } return state; },This same cumulative pattern needs to be applied to the earlier version branches as well.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/stores/configStore.ts` around lines 427 - 494, The migrate function in configStore.ts returns early inside each version check, which prevents later migrations (e.g., adding acknowledgedFeatures) from running; change migrate to apply migrations cumulatively by starting with let state = persistedState as ExtensionConfig and for each if (version < N) block mutate/assign properties on that state (e.g., add pinnedProperties with defaultPinnedProfile, add traits to profiles using PinnedPropertiesConfig, set preferredEventDetailView from defaultConfig, add dismissedOnboardingModals, set sectionDefaults = defaultSectionDefaults, set exportSections = DEFAULT_EXPORT_SECTIONS, set acknowledgedFeatures = defaultConfig.acknowledgedFeatures) without returning, and after all checks return the fully migrated state; update references inside the migrate function to use the same state variable so isFeatureAcknowledged() won't see undefined.src/pages/panel/Panel.tsx (1)
360-403:⚠️ Potential issue | 🟠 MajorKeep export state independent from active filters, and make ordering explicit.
selectedExportIdssurvives search/hide changes, but bothselectedEventsand the toolbar count are derived fromfilteredEvents. That means a selected event disappears from the export as soon as it is filtered out, and the export button can drop to0/disable even though the selection still exists. On top of that,.reverse()only works if the list is always newest-first; if the panel is already chronological, the export header/dividers become backwards.Proposed fix
+ const totalSelectedCount = useMemo(() => { + if (selectedExportIds.size === 0) return 0; + return events.filter((e) => selectedExportIds.has(e.id)).length; + }, [events, selectedExportIds]); + // Count of selected events that are currently visible (for toolbar display and export) const visibleSelectedCount = useMemo(() => { if (selectedExportIds.size === 0) return 0; return filteredEvents.filter((e) => selectedExportIds.has(e.id)).length; }, [filteredEvents, selectedExportIds]); // Selected events for the export modal const selectedEvents = useMemo( - () => filteredEvents.filter((e) => selectedExportIds.has(e.id)).reverse(), - [filteredEvents, selectedExportIds] + () => + events + .filter((e) => selectedExportIds.has(e.id)) + .sort((a, b) => a.capturedAt - b.capturedAt), + [events, selectedExportIds] ); ... {isExportMode && ( <ExportToolbar - selectedCount={visibleSelectedCount} + selectedCount={totalSelectedCount} totalCount={filteredEvents.length} onSelectAll={handleSelectAll} onDeselectAll={handleDeselectAll} onExport={handleExportSelected} onCancel={handleCancelExport} /> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/panel/Panel.tsx` around lines 360 - 403, The bug is that selectedEvents and visibleSelectedCount are derived from filteredEvents so selections vanish when filters change and .reverse() makes ordering implicit; instead compute selectedEvents from the master events list using selectedExportIds (e.g. selectedEvents = useMemo(() => events.filter(e => selectedExportIds.has(e.id)), [events, selectedExportIds]) ) so export state is independent of filters, keep visibleSelectedCount as the intersection with filteredEvents (filteredEvents.filter(e => selectedExportIds.has(e.id)).length), and remove the .reverse() call—make ordering explicit by sorting selectedEvents with a clear comparator (e.g. by timestamp or by the index/order in events) so ExportToolbar, handleExportSelected, visibleSelectedCount and selectedEvents all behave deterministically regardless of the active filters.
🧹 Nitpick comments (3)
src/stores/configStore.test.ts (1)
150-181: Add an upgrade-path regression test for the new persisted fields.These cases cover fresh state only. The risky path here is rehydrating v7/v8 data, because
exportSectionsandacknowledgedFeaturesare now read immediately during render. A small migration test would catch upgrade regressions before release.Also applies to: 512-555
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/stores/configStore.test.ts` around lines 150 - 181, Add a regression test that simulates rehydrating old persisted state (v7/v8) missing the new persisted fields and assert the store initializes them to defaults; specifically, in the test suite that uses useConfigStore and DEFAULT_EXPORT_SECTIONS, create a mocked persisted state object lacking exportSections and acknowledgedFeatures, call the store rehydrate/initialization path (or set state directly to simulate rehydration) and verify useConfigStore.getState().exportSections === DEFAULT_EXPORT_SECTIONS and useConfigStore.getState().acknowledgedFeatures equals the expected default; ensure the test exercises the immediate-read path during render so migrations run.src/lib/exportFormatter.ts (2)
53-64: Avoid duplicating reload-boundary logic.Line 51 notes this is “same logic” as another module. Keeping duplicate implementations will drift over time; move this helper to a shared utility and reuse it from both places.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/exportFormatter.ts` around lines 53 - 64, The function findReloadBetween duplicates reload-boundary logic present in another module; extract this helper into a shared utility (e.g., create a new util function exported from a common utils file) and replace the local implementation in exportFormatter.ts with an import of that shared function, updating all other modules that currently reimplement the same logic to import and use the shared findReloadBetween utility instead; ensure the exported function keeps the same signature (prev: SegmentEvent, current: SegmentEvent, reloadTimestamps: number[]) so callers (including exportFormatter's callers) need only swap the local definition for the imported one.
285-339: Make event ordering deterministic inside the formatter.Line 308 states events are chronological, but the function currently trusts input order. Sorting by
capturedAtlocally makes dividers/header ranges reliable regardless of caller behavior.Proposed fix
export function formatEventsAsMarkdown( events: SegmentEvent[], reloadTimestamps: number[], sections: ExportSections ): string { if (events.length === 0) return ''; - const domain = extractDomainFromEvents(events); - const firstTs = events[0].timestamp; - const lastTs = events[events.length - 1].timestamp; + const orderedEvents = [...events].sort((a, b) => a.capturedAt - b.capturedAt); + const domain = extractDomainFromEvents(orderedEvents); + const firstTs = orderedEvents[0].timestamp; + const lastTs = orderedEvents[orderedEvents.length - 1].timestamp; @@ - for (let i = 0; i < events.length; i++) { - const event = events[i]; - const prev = i > 0 ? events[i - 1] : undefined; + for (let i = 0; i < orderedEvents.length; i++) { + const event = orderedEvents[i]; + const prev = i > 0 ? orderedEvents[i - 1] : undefined;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/exportFormatter.ts` around lines 285 - 339, The formatter trusts input order but must enforce deterministic chronological ordering; inside formatEventsAsMarkdown create a locally sorted array (e.g., sortedEvents = [...events].sort((a,b) => (a.capturedAt ?? a.timestamp) - (b.capturedAt ?? b.timestamp))) and use sortedEvents everywhere instead of the original events when computing domain, firstTs/lastTs, iterating for dividers, and calling formatEventMarkdown; this ensures dividers, header ranges and navigation detection (urlsAreDifferent, findReloadBetween, getEventUrl, extractPathFromUrl) are reliable regardless of caller ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/exportFormatter.test.ts`:
- Around line 82-95: The test locks in incorrect pluralization ("1 events");
update the formatter to singularize when count === 1 and update the test to
expect "1 event". Specifically, modify formatEventsAsMarkdown to compute a
verbatim header using the events.length (use "event" vs "events" when
events.length === 1) and ensure the output still includes the domain and date
range; then change the test assertion (which uses mockEvent1 and
DEFAULT_EXPORT_SECTIONS) to expect '> 1 event from **example.com**' instead of
'> 1 events'.
In `@src/lib/exportFormatter.ts`:
- Around line 211-214: The conditional guards around required SegmentEvent
fields are unnecessary; remove the if checks for event.url, event.provider, and
event.capturedAt in exportFormatter.ts and always append their formatted lines
to the lines array (e.g., push the URL, provider, and a new
Date(event.capturedAt).toISOString())—locate the logic inside the function that
builds lines for an event (references: event.url, event.provider,
event.capturedAt and the surrounding lines.push calls) and simplify by directly
pushing those values without the truthy conditionals.
---
Outside diff comments:
In `@src/pages/panel/Panel.tsx`:
- Around line 360-403: The bug is that selectedEvents and visibleSelectedCount
are derived from filteredEvents so selections vanish when filters change and
.reverse() makes ordering implicit; instead compute selectedEvents from the
master events list using selectedExportIds (e.g. selectedEvents = useMemo(() =>
events.filter(e => selectedExportIds.has(e.id)), [events, selectedExportIds]) )
so export state is independent of filters, keep visibleSelectedCount as the
intersection with filteredEvents (filteredEvents.filter(e =>
selectedExportIds.has(e.id)).length), and remove the .reverse() call—make
ordering explicit by sorting selectedEvents with a clear comparator (e.g. by
timestamp or by the index/order in events) so ExportToolbar,
handleExportSelected, visibleSelectedCount and selectedEvents all behave
deterministically regardless of the active filters.
In `@src/stores/configStore.ts`:
- Around line 427-494: The migrate function in configStore.ts returns early
inside each version check, which prevents later migrations (e.g., adding
acknowledgedFeatures) from running; change migrate to apply migrations
cumulatively by starting with let state = persistedState as ExtensionConfig and
for each if (version < N) block mutate/assign properties on that state (e.g.,
add pinnedProperties with defaultPinnedProfile, add traits to profiles using
PinnedPropertiesConfig, set preferredEventDetailView from defaultConfig, add
dismissedOnboardingModals, set sectionDefaults = defaultSectionDefaults, set
exportSections = DEFAULT_EXPORT_SECTIONS, set acknowledgedFeatures =
defaultConfig.acknowledgedFeatures) without returning, and after all checks
return the fully migrated state; update references inside the migrate function
to use the same state variable so isFeatureAcknowledged() won't see undefined.
---
Nitpick comments:
In `@src/lib/exportFormatter.ts`:
- Around line 53-64: The function findReloadBetween duplicates reload-boundary
logic present in another module; extract this helper into a shared utility
(e.g., create a new util function exported from a common utils file) and replace
the local implementation in exportFormatter.ts with an import of that shared
function, updating all other modules that currently reimplement the same logic
to import and use the shared findReloadBetween utility instead; ensure the
exported function keeps the same signature (prev: SegmentEvent, current:
SegmentEvent, reloadTimestamps: number[]) so callers (including
exportFormatter's callers) need only swap the local definition for the imported
one.
- Around line 285-339: The formatter trusts input order but must enforce
deterministic chronological ordering; inside formatEventsAsMarkdown create a
locally sorted array (e.g., sortedEvents = [...events].sort((a,b) =>
(a.capturedAt ?? a.timestamp) - (b.capturedAt ?? b.timestamp))) and use
sortedEvents everywhere instead of the original events when computing domain,
firstTs/lastTs, iterating for dividers, and calling formatEventMarkdown; this
ensures dividers, header ranges and navigation detection (urlsAreDifferent,
findReloadBetween, getEventUrl, extractPathFromUrl) are reliable regardless of
caller ordering.
In `@src/stores/configStore.test.ts`:
- Around line 150-181: Add a regression test that simulates rehydrating old
persisted state (v7/v8) missing the new persisted fields and assert the store
initializes them to defaults; specifically, in the test suite that uses
useConfigStore and DEFAULT_EXPORT_SECTIONS, create a mocked persisted state
object lacking exportSections and acknowledgedFeatures, call the store
rehydrate/initialization path (or set state directly to simulate rehydration)
and verify useConfigStore.getState().exportSections === DEFAULT_EXPORT_SECTIONS
and useConfigStore.getState().acknowledgedFeatures equals the expected default;
ensure the test exercises the immediate-read path during render so migrations
run.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7100b0df-d2e0-4737-a36f-a72e72e289d1
📒 Files selected for processing (26)
package.jsonsrc/components/ThemedJsonView.tsxsrc/hooks/index.tssrc/hooks/useNewFeatureBadge.tssrc/lib/exportFormatter.test.tssrc/lib/exportFormatter.tssrc/lib/index.tssrc/pages/options/Options.tsxsrc/pages/options/components/ResetButton.tsxsrc/pages/panel/Panel.tsxsrc/pages/panel/components/ActionButtons.tsxsrc/pages/panel/components/EventList/EventList.tsxsrc/pages/panel/components/EventRow.tsxsrc/pages/panel/components/EventRowHeader.tsxsrc/pages/panel/components/ExportModal.tsxsrc/pages/panel/components/ExportModalSectionToggle.tsxsrc/pages/panel/components/ExportToolbar.tsxsrc/pages/panel/components/Header.tsxsrc/pages/panel/components/OnboardingSystem.tsxsrc/pages/panel/components/WelcomeOnboardingModal.tsxsrc/pages/panel/components/detail/primitives/PropertyRow/PropertyRow.tsxsrc/pages/panel/components/index.tssrc/stores/configStore.test.tssrc/stores/configStore.tssrc/stores/tabStore.test.tssrc/test/setup.ts
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/stores/configStore.ts (1)
427-495:⚠️ Potential issue | 🟠 MajorRun migrations cumulatively instead of returning after the first match.
These branches return immediately, so a user upgrading from version 1 or 2 never reaches the later transforms. That leaves nested shape changes unapplied—for example the v3 addition of
pinnedProperties.*.traits—sotogglePin('traits', ...)can still hitundefinedafter upgrade.💡 Proposed fix
- migrate: (persistedState, version) => { - const state = persistedState as ExtensionConfig; + migrate: (persistedState, version) => { + let state = persistedState as Partial<ExtensionConfig>; if (version < 2) { - return { + state = { ...state, pinnedProperties: { default: defaultPinnedProfile, }, }; } if (version < 3) { const updatedPinnedProperties: PinnedPropertiesConfig = {}; for (const [profileKey, profile] of Object.entries( - state.pinnedProperties + state.pinnedProperties ?? {} )) { updatedPinnedProperties[profileKey] = { ...profile, - traits: [], + traits: profile.traits ?? [], }; } - return { + state = { ...state, pinnedProperties: updatedPinnedProperties, }; } if (version < 8) { - return { - ...state, - exportSections: DEFAULT_EXPORT_SECTIONS, - }; + state = { + ...state, + exportSections: DEFAULT_EXPORT_SECTIONS, + }; } if (version < 9) { - return { + state = { ...state, acknowledgedFeatures: defaultConfig.acknowledgedFeatures, }; } - return state; + return state as ExtensionConfig; },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/stores/configStore.ts` around lines 427 - 495, The migrate function currently returns early inside each version branch so later migrations are skipped; change it to apply migrations cumulatively by treating persistedState as a mutable state variable (e.g. let state = persistedState as ExtensionConfig) and for each if (version < N) block update state = { ...state, <changes> } instead of returning immediately, and after all if blocks return the final state; update all branches that now return (v2..v9) to assign to state and remove intermediate returns so pinnedProperties, traits, preferredEventDetailView, dismissedOnboardingModals, sectionDefaults, exportSections, and acknowledgedFeatures are all applied (references: migrate, state/persistedState, defaultPinnedProfile, defaultConfig, defaultSectionDefaults, DEFAULT_EXPORT_SECTIONS).
🧹 Nitpick comments (2)
src/pages/panel/components/OnboardingSystem.tsx (1)
84-105: Deduplicate the close-side effects.
handleOpenChange(false)andhandleDismiss()now perform the same dismiss/acknowledge sequence. Pulling that into one helper will keep the two paths from drifting.♻️ Possible cleanup
+ const closeAndAcknowledge = () => { + dismiss(); + acknowledgeFeatures(); + setIsOpen(false); + }; + const handleOpenChange = (open: boolean) => { - setIsOpen(open); - - // If closing, dismiss the modal and acknowledge features - if (!open) { - dismiss(); - acknowledgeFeatures(); - } + if (open) { + setIsOpen(true); + return; + } + + closeAndAcknowledge(); }; const handleDismiss = () => { - dismiss(); - acknowledgeFeatures(); - setIsOpen(false); + closeAndAcknowledge(); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/panel/components/OnboardingSystem.tsx` around lines 84 - 105, The close-side effects (dismiss, acknowledgeFeatures, setIsOpen(false)) are duplicated between handleOpenChange when open is false and handleDismiss; extract them into a single helper (e.g., closeAndAcknowledge) that calls dismiss(), acknowledgeFeatures(), and setIsOpen(false), then replace the duplicated code in handleOpenChange and handleDismiss to call this new helper; keep references to acknowledgeFeatures, dismiss, handleOpenChange, and handleDismiss so callers remain unchanged.src/pages/panel/components/ExportModal.tsx (1)
183-215: Consider usingrole="radiogroup"for mode selector.The two mode buttons function as mutually exclusive options. While
aria-pressedworks, semanticrole="radiogroup"withrole="radio"on buttons would better convey the single-selection constraint to assistive technologies.♻️ Proposed accessibility improvement
<div - role="group" - aria-label="Export mode" + role="radiogroup" + aria-label="Export format" className="flex gap-1 rounded-md border border-border p-1" > <button type="button" - aria-pressed={mode === 'smart'} + role="radio" + aria-checked={mode === 'smart'} className={cn( ... )} onClick={() => setMode('smart')} > Smart Export </button> <button type="button" - aria-pressed={mode === 'raw'} + role="radio" + aria-checked={mode === 'raw'} className={cn( ... )} onClick={() => setMode('raw')} > Raw JSON </button> </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/panel/components/ExportModal.tsx` around lines 183 - 215, The mode selector in ExportModal.tsx currently uses aria-pressed on two buttons (mode state and setMode) but should be made into a semantic radio group: wrap the two buttons container with role="radiogroup" and give each button role="radio" plus aria-checked={mode === 'smart'|'raw'} (and keep onClick calling setMode('smart'/'raw')); ensure keyboard behavior is preserved and update any styling/aria-pressed usage to aria-checked so assistive tech understands the single-selection radio semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/exportFormatter.ts`:
- Around line 301-303: The header always prints "${events.length} events" which
is incorrect for singular; update the construction used in the lines.push call
so it pluralizes "event" based on events.length (use a ternary or pluralization
helper) and keep domain, firstTs and lastTs unchanged; locate the lines.push
invocation that references events.length, domain, firstTs and lastTs and change
the text to use "event" when events.length === 1 and "events" otherwise.
---
Outside diff comments:
In `@src/stores/configStore.ts`:
- Around line 427-495: The migrate function currently returns early inside each
version branch so later migrations are skipped; change it to apply migrations
cumulatively by treating persistedState as a mutable state variable (e.g. let
state = persistedState as ExtensionConfig) and for each if (version < N) block
update state = { ...state, <changes> } instead of returning immediately, and
after all if blocks return the final state; update all branches that now return
(v2..v9) to assign to state and remove intermediate returns so pinnedProperties,
traits, preferredEventDetailView, dismissedOnboardingModals, sectionDefaults,
exportSections, and acknowledgedFeatures are all applied (references: migrate,
state/persistedState, defaultPinnedProfile, defaultConfig,
defaultSectionDefaults, DEFAULT_EXPORT_SECTIONS).
---
Nitpick comments:
In `@src/pages/panel/components/ExportModal.tsx`:
- Around line 183-215: The mode selector in ExportModal.tsx currently uses
aria-pressed on two buttons (mode state and setMode) but should be made into a
semantic radio group: wrap the two buttons container with role="radiogroup" and
give each button role="radio" plus aria-checked={mode === 'smart'|'raw'} (and
keep onClick calling setMode('smart'/'raw')); ensure keyboard behavior is
preserved and update any styling/aria-pressed usage to aria-checked so assistive
tech understands the single-selection radio semantics.
In `@src/pages/panel/components/OnboardingSystem.tsx`:
- Around line 84-105: The close-side effects (dismiss, acknowledgeFeatures,
setIsOpen(false)) are duplicated between handleOpenChange when open is false and
handleDismiss; extract them into a single helper (e.g., closeAndAcknowledge)
that calls dismiss(), acknowledgeFeatures(), and setIsOpen(false), then replace
the duplicated code in handleOpenChange and handleDismiss to call this new
helper; keep references to acknowledgeFeatures, dismiss, handleOpenChange, and
handleDismiss so callers remain unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1398131b-dcff-494a-829c-783b2dd06ce3
📒 Files selected for processing (26)
package.jsonsrc/components/ThemedJsonView.tsxsrc/hooks/index.tssrc/hooks/useNewFeatureBadge.tssrc/lib/exportFormatter.test.tssrc/lib/exportFormatter.tssrc/lib/index.tssrc/pages/options/Options.tsxsrc/pages/options/components/ResetButton.tsxsrc/pages/panel/Panel.tsxsrc/pages/panel/components/ActionButtons.tsxsrc/pages/panel/components/EventList/EventList.tsxsrc/pages/panel/components/EventRow.tsxsrc/pages/panel/components/EventRowHeader.tsxsrc/pages/panel/components/ExportModal.tsxsrc/pages/panel/components/ExportModalSectionToggle.tsxsrc/pages/panel/components/ExportToolbar.tsxsrc/pages/panel/components/Header.tsxsrc/pages/panel/components/OnboardingSystem.tsxsrc/pages/panel/components/WelcomeOnboardingModal.tsxsrc/pages/panel/components/detail/primitives/PropertyRow/PropertyRow.tsxsrc/pages/panel/components/index.tssrc/stores/configStore.test.tssrc/stores/configStore.tssrc/stores/tabStore.test.tssrc/test/setup.ts
This pull request introduces a comprehensive "Export Events" feature to the analytics panel, allowing users to select events from the timeline and export them in either Markdown or JSON format. The implementation includes UI updates for selection and export controls, event selection logic (including shift-click range selection), and export formatting utilities. It also introduces new components and updates existing ones to support the export workflow.
Key changes include:
1. Export Functionality Implementation
exportFormatter.tswith utilities to format selected events as Markdown or raw JSON, including configurable export sections for properties, traits, context, and metadata.2. UI & Event Selection Enhancements
Panel.tsx, supporting toggling export mode, selecting/deselecting events (including shift-click range selection), and showing the export modal. [1] [2] [3] [4] [5] [6]3. Event List and Row Component Updates
EventListandEventRowcomponents to support export mode, including selection checkboxes, highlighting, and selection toggling via props and callbacks. (Fa610b5fL15R15, [1] [2] [3] [4]4. Versioning
1.0.0to1.1.0to reflect the addition of major new export functionality.Demo
Export example:
Smart Export Example only properties exported
Analytics Events Export
module_viewed (track)
module_viewed (track)
module_viewed (track)
Page Reload
Page: / (page)
TikTok Page View (track)
module_viewed (track)
Same 6 events, smart export is much more token efficient to send to LLMs to check against Specs, export only what matters to you.
Raw Export Example
[ { "timestamp": "2026-04-11T14:49:08.716Z", "integrations": {}, "event": "module_viewed", "type": "track", "properties": { "moduleLevel": 1, "moduleName": "package", "moduleIndex": 4, "moduleLocation": 2884, "site": "main", "appVersion": "4c4a981e0e0f45cd40c4f6c823cac0bcd405424c", "sessionId": "1ee4e0ea-933c-4342-a1f9-9b7b9d0f1585", "sessionVersion": 1 }, "context": { "app": { "version": "4c4a981e0e0f45cd40c4f6c823cac0bcd405424c" }, "page": { "path": "/", "referrer": "", "search": "", "title": "Home | Example", "url": "https://www.example.com/" }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", "userAgentData": { "brands": [ { "brand": "Chromium", "version": "146" }, { "brand": "Not-A.Brand", "version": "24" }, { "brand": "Google Chrome", "version": "146" } ], "mobile": false, "platform": "macOS" }, "locale": "en-US", "library": { "name": "analytics.js", "version": "next-1.81.0" }, "timezone": "America/Montevideo" }, "messageId": "ajs-next-1775918948716-913cc0a1-c5f1-4059-8295-b3771abd94fb", "anonymousId": "27e42200-1fb5-11f1-9364-ff1ca8496075", "writeKey": "mm6L09wAd0PG1RKTh8KJmkzsFWVl8RnY", "userId": null, "sentAt": "2026-04-11T14:49:08.721Z", "_metadata": { "bundled": [ "Bing Ads", "Facebook Pixel", "FullStory", "Google AdWords New", "Google Tag Manager", "Segment.io" ], "unbundled": [], "bundledIds": [ "5a68fcd122633e0001fb4e9a", "xD77vxzsvt", "5a43af42a83513000134da51", "612378eb619898849d57b5c1", "GiKBeBwYRX" ] } }, { "timestamp": "2026-04-11T14:49:09.299Z", "integrations": {}, "event": "module_viewed", "type": "track", "properties": { "moduleLevel": 1, "moduleName": "treats", "moduleIndex": 5, "moduleLocation": 3638, "site": "main", "appVersion": "4c4a981e0e0f45cd40c4f6c823cac0bcd405424c", "sessionId": "1ee4e0ea-933c-4342-a1f9-9b7b9d0f1585", "sessionVersion": 1 }, "context": { "app": { "version": "4c4a981e0e0f45cd40c4f6c823cac0bcd405424c" }, "page": { "path": "/", "referrer": "", "search": "", "title": "Home | Example", "url": "https://www.example.com/" }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", "userAgentData": { "brands": [ { "brand": "Chromium", "version": "146" }, { "brand": "Not-A.Brand", "version": "24" }, { "brand": "Google Chrome", "version": "146" } ], "mobile": false, "platform": "macOS" }, "locale": "en-US", "library": { "name": "analytics.js", "version": "next-1.81.0" }, "timezone": "America/Montevideo" }, "messageId": "ajs-next-1775918949299-c5f11059-4295-4377-9abd-94fb51e7bdf1", "anonymousId": "27e42200-1fb5-11f1-9364-ff1ca8496075", "writeKey": "mm6L09wAd0PG1RKTh8KJmkzsFWVl8RnY", "userId": null, "sentAt": "2026-04-11T14:49:09.302Z", "_metadata": { "bundled": [ "Bing Ads", "Facebook Pixel", "FullStory", "Google AdWords New", "Google Tag Manager", "Segment.io" ], "unbundled": [], "bundledIds": [ "5a68fcd122633e0001fb4e9a", "xD77vxzsvt", "5a43af42a83513000134da51", "612378eb619898849d57b5c1", "GiKBeBwYRX" ] } }, { "timestamp": "2026-04-11T14:49:09.600Z", "integrations": {}, "event": "module_viewed", "type": "track", "properties": { "moduleLevel": 1, "moduleName": "how_it_works", "moduleIndex": 6, "moduleLocation": 4358, "site": "main", "appVersion": "4c4a981e0e0f45cd40c4f6c823cac0bcd405424c", "sessionId": "1ee4e0ea-933c-4342-a1f9-9b7b9d0f1585", "sessionVersion": 1 }, "context": { "app": { "version": "4c4a981e0e0f45cd40c4f6c823cac0bcd405424c" }, "page": { "path": "/", "referrer": "", "search": "", "title": "Home | Example", "url": "https://www.example.com/" }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", "userAgentData": { "brands": [ { "brand": "Chromium", "version": "146" }, { "brand": "Not-A.Brand", "version": "24" }, { "brand": "Google Chrome", "version": "146" } ], "mobile": false, "platform": "macOS" }, "locale": "en-US", "library": { "name": "analytics.js", "version": "next-1.81.0" }, "timezone": "America/Montevideo" }, "messageId": "ajs-next-1775918949600-10594295-b377-4abd-94fb-51e7bdf113c1", "anonymousId": "27e42200-1fb5-11f1-9364-ff1ca8496075", "writeKey": "mm6L09wAd0PG1RKTh8KJmkzsFWVl8RnY", "userId": null, "sentAt": "2026-04-11T14:49:09.603Z", "_metadata": { "bundled": [ "Bing Ads", "Facebook Pixel", "FullStory", "Google AdWords New", "Google Tag Manager", "Segment.io" ], "unbundled": [], "bundledIds": [ "5a68fcd122633e0001fb4e9a", "xD77vxzsvt", "5a43af42a83513000134da51", "612378eb619898849d57b5c1", "GiKBeBwYRX" ] } }, { "timestamp": "2026-04-11T14:49:41.652Z", "integrations": {}, "type": "page", "properties": { "path": "/", "referrer": "", "search": "", "title": "Home | Example", "url": "https://www.example.com/", "city": "Montevideo", "state": "MO", "zip": "11100", "country": "UY", "sessionId": "1ee4e0ea-933c-4342-a1f9-9b7b9d0f1585", "sessionVersion": 1, "name": "/" }, "name": "/", "context": { "page": { "path": "/", "referrer": "", "search": "", "title": "Home | Example", "url": "https://www.example.com/" }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", "userAgentData": { "brands": [ { "brand": "Chromium", "version": "146" }, { "brand": "Not-A.Brand", "version": "24" }, { "brand": "Google Chrome", "version": "146" } ], "mobile": false, "platform": "macOS" }, "locale": "en-US", "library": { "name": "analytics.js", "version": "next-1.81.0" }, "timezone": "America/Montevideo" }, "messageId": "ajs-next-1775918981652-f353477c-400d-461c-853e-5d04cf2627c6", "anonymousId": "27e42200-1fb5-11f1-9364-ff1ca8496075", "writeKey": "mm6L09wAd0PG1RKTh8KJmkzsFWVl8RnY", "userId": null, "sentAt": "2026-04-11T14:49:41.681Z", "_metadata": { "bundled": [ "Bing Ads", "Facebook Pixel", "FullStory", "Google AdWords New", "Google Tag Manager", "Segment.io" ], "unbundled": [], "bundledIds": [ "5a68fcd122633e0001fb4e9a", "xD77vxzsvt", "5a43af42a83513000134da51", "612378eb619898849d57b5c1", "GiKBeBwYRX" ] } }, { "timestamp": "2026-04-11T14:49:41.654Z", "integrations": {}, "event": "TikTok Page View", "type": "track", "properties": { "email": null, "event_id": "b293d485-fa69-401c-afd0-146d84b00f6c", "appVersion": "4c4a981e0e0f45cd40c4f6c823cac0bcd405424c", "sessionId": "1ee4e0ea-933c-4342-a1f9-9b7b9d0f1585", "sessionVersion": 1 }, "context": { "app": { "version": "4c4a981e0e0f45cd40c4f6c823cac0bcd405424c" }, "page": { "path": "/", "referrer": "", "search": "", "title": "Home | Example", "url": "https://www.example.com/" }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", "userAgentData": { "brands": [ { "brand": "Chromium", "version": "146" }, { "brand": "Not-A.Brand", "version": "24" }, { "brand": "Google Chrome", "version": "146" } ], "mobile": false, "platform": "macOS" }, "locale": "en-US", "library": { "name": "analytics.js", "version": "next-1.81.0" }, "timezone": "America/Montevideo" }, "messageId": "ajs-next-1775918981654-477c400d-f61c-453e-9d04-cf2627c6aff3", "anonymousId": "27e42200-1fb5-11f1-9364-ff1ca8496075", "writeKey": "mm6L09wAd0PG1RKTh8KJmkzsFWVl8RnY", "userId": null, "sentAt": "2026-04-11T14:49:41.683Z", "_metadata": { "bundled": [ "Bing Ads", "Facebook Pixel", "FullStory", "Google AdWords New", "Google Tag Manager", "Segment.io" ], "unbundled": [], "bundledIds": [ "5a68fcd122633e0001fb4e9a", "xD77vxzsvt", "5a43af42a83513000134da51", "612378eb619898849d57b5c1", "GiKBeBwYRX" ] } }, { "timestamp": "2026-04-11T14:49:41.656Z", "integrations": {}, "event": "module_viewed", "type": "track", "properties": { "moduleLevel": 1, "moduleName": "hero", "moduleIndex": 1, "moduleLocation": 119, "site": "main", "appVersion": "4c4a981e0e0f45cd40c4f6c823cac0bcd405424c", "sessionId": "1ee4e0ea-933c-4342-a1f9-9b7b9d0f1585", "sessionVersion": 1 }, "context": { "app": { "version": "4c4a981e0e0f45cd40c4f6c823cac0bcd405424c" }, "page": { "path": "/", "referrer": "", "search": "", "title": "Home | Example", "url": "https://www.example.com/" }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", "userAgentData": { "brands": [ { "brand": "Chromium", "version": "146" }, { "brand": "Not-A.Brand", "version": "24" }, { "brand": "Google Chrome", "version": "146" } ], "mobile": false, "platform": "macOS" }, "locale": "en-US", "library": { "name": "analytics.js", "version": "next-1.81.0" }, "timezone": "America/Montevideo" }, "messageId": "ajs-next-1775918981656-400df61c-853e-4d04-8f26-27c6aff3bd9b", "anonymousId": "27e42200-1fb5-11f1-9364-ff1ca8496075", "writeKey": "mm6L09wAd0PG1RKTh8KJmkzsFWVl8RnY", "userId": null, "sentAt": "2026-04-11T14:49:41.684Z", "_metadata": { "bundled": [ "Bing Ads", "Facebook Pixel", "FullStory", "Google AdWords New", "Google Tag Manager", "Segment.io" ], "unbundled": [], "bundledIds": [ "5a68fcd122633e0001fb4e9a", "xD77vxzsvt", "5a43af42a83513000134da51", "612378eb619898849d57b5c1", "GiKBeBwYRX" ] } } ]Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores