Skip to content

feat-export-timeline#35

Merged
agch-dev merged 13 commits into
mainfrom
feat-export-timeline
Apr 12, 2026
Merged

feat-export-timeline#35
agch-dev merged 13 commits into
mainfrom
feat-export-timeline

Conversation

@agch-dev
Copy link
Copy Markdown
Owner

@agch-dev agch-dev commented Apr 11, 2026

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

  • Added exportFormatter.ts with utilities to format selected events as Markdown or raw JSON, including configurable export sections for properties, traits, context, and metadata.
  • Export functions and types are now re-exported from the main library index for use throughout the app.

2. UI & Event Selection Enhancements

  • Introduced export mode state and selection logic in 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]
  • Added an "Export" button to the action bar and passed export-related props to relevant components for UI state management. [1] [2] [3] [4] [5]

3. Event List and Row Component Updates

  • Updated EventList and EventRow components to support export mode, including selection checkboxes, highlighting, and selection toggling via props and callbacks. (Fa610b5fL15R15, [1] [2] [3] [4]

4. Versioning

  • Bumped the package version from 1.0.0 to 1.1.0 to reflect the addition of major new export functionality.

Demo

image image

Export example:

Smart Export Example only properties exported

Analytics Events Export

6 events from www.example.com captured between 2026-04-11T14:49:08.716Z and 2026-04-11T14:49:41.656Z.
This export contains analytics tracking events captured from a web application’s Segment/RudderStack SDK.
Events are listed chronologically. Navigation changes and page reloads are marked with dividers.

module_viewed (track)

  • Timestamp: 2026-04-11T14:49:08.716Z
  • Properties:
    • moduleLevel: 1
    • moduleName: package
    • moduleIndex: 4
    • moduleLocation: 2884
    • site: main
    • appVersion: 4c4a981e0e0f45cd40c4f6c823cac0bcd405424c
    • sessionId: 1ee4e0ea-933c-4342-a1f9-9b7b9d0f1585
    • sessionVersion: 1

module_viewed (track)

  • Timestamp: 2026-04-11T14:49:09.299Z
  • Properties:
    • moduleLevel: 1
    • moduleName: treats
    • moduleIndex: 5
    • moduleLocation: 3638
    • site: main
    • appVersion: 4c4a981e0e0f45cd40c4f6c823cac0bcd405424c
    • sessionId: 1ee4e0ea-933c-4342-a1f9-9b7b9d0f1585
    • sessionVersion: 1

module_viewed (track)

  • Timestamp: 2026-04-11T14:49:09.600Z
  • Properties:
    • moduleLevel: 1
    • moduleName: how_it_works
    • moduleIndex: 6
    • moduleLocation: 4358
    • site: main
    • appVersion: 4c4a981e0e0f45cd40c4f6c823cac0bcd405424c
    • sessionId: 1ee4e0ea-933c-4342-a1f9-9b7b9d0f1585
    • sessionVersion: 1

Page Reload

Page: / (page)

  • Timestamp: 2026-04-11T14:49:41.652Z
  • 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: /

TikTok Page View (track)

  • Timestamp: 2026-04-11T14:49:41.654Z
  • Properties:
    • email: null
    • event_id: b293d485-fa69-401c-afd0-146d84b00f6c
    • appVersion: 4c4a981e0e0f45cd40c4f6c823cac0bcd405424c
    • sessionId: 1ee4e0ea-933c-4342-a1f9-9b7b9d0f1585
    • sessionVersion: 1

module_viewed (track)

  • Timestamp: 2026-04-11T14:49:41.656Z
  • Properties:
    • moduleLevel: 1
    • moduleName: hero
    • moduleIndex: 1
    • moduleLocation: 119
    • site: main
    • appVersion: 4c4a981e0e0f45cd40c4f6c823cac0bcd405424c
    • sessionId: 1ee4e0ea-933c-4342-a1f9-9b7b9d0f1585
    • sessionVersion: 1

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

    • Timeline export: Smart Markdown + raw JSON, multi-event selection (shift+click), export toolbar/modal, configurable export sections, and a new badge hook for export.
    • Onboarding updated with a “Timeline Export” entry and acknowledgment flow; copy-to-clipboard with success feedback.
  • Bug Fixes

    • Improved timeout/timer cleanup across UI to prevent stale updates after unmount.
  • Tests

    • Added comprehensive export formatting tests and expanded config store coverage.
  • Chores

    • Version bumped to 1.1.0

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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 via src/lib/index.ts.
  • Adds export-mode UI + selection state in Panel.tsx, plus new ExportToolbar/ExportModal components 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 added exportSections preference. This means users can’t return export section toggles to defaults via the existing reset path. Consider including exportSections: defaultConfig.exportSections in 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.

Comment thread src/stores/configStore.ts
Comment thread src/lib/exportFormatter.ts
Comment thread src/pages/panel/components/ExportModal.tsx
Comment thread src/pages/panel/components/ExportModal.tsx Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 added exportSections preference (it only sets maxEvents, theme, and preferredEventDetailView). Since the Options UI describes this action as “Reset all settings to default values”, users who changed export section toggles will keep their custom exportSections after reset. Consider including exportSections: defaultConfig.exportSections (or DEFAULT_EXPORT_SECTIONS) in the reset payload (and, if intended, other user-visible settings like sectionDefaults/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.

Comment thread src/test/setup.ts Outdated
Comment thread src/lib/exportFormatter.ts
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +1 to +4
import { cn } from '@src/lib';

import { Label, Switch } from '@components/ui';

Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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';

Copilot uses AI. Check for mistakes.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7232079-a375-4b89-b82b-58c46e157812

📥 Commits

Reviewing files that changed from the base of the PR and between 7481108 and 04887dd.

📒 Files selected for processing (2)
  • src/lib/exportFormatter.test.ts
  • src/lib/exportFormatter.ts
✅ Files skipped from review due to trivial changes (2)
  • src/lib/exportFormatter.test.ts
  • src/lib/exportFormatter.ts

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Export Core Formatting
src/lib/exportFormatter.ts, src/lib/exportFormatter.test.ts, src/lib/index.ts
New exporter module: ExportSections + DEFAULT_EXPORT_SECTIONS, formatEventsAsMarkdown() and formatEventsAsJson() implementations, comprehensive tests, and re-exports from lib index.
Export UI Components
src/pages/panel/components/ExportModal.tsx, src/pages/panel/components/ExportToolbar.tsx, src/pages/panel/components/ExportModalSectionToggle.tsx, src/pages/panel/components/index.ts
New ExportModal, ExportToolbar, and SectionToggle components; modal supports smart/raw modes, section parent/child toggles, clipboard copy logic, and barrel exports added.
Panel Selection & Integration
src/pages/panel/Panel.tsx, src/pages/panel/components/EventList/EventList.tsx, src/pages/panel/components/EventRow.tsx, src/pages/panel/components/EventRowHeader.tsx, src/pages/panel/components/Header.tsx, src/pages/panel/components/ActionButtons.tsx
Adds export mode state, selection handling (including shift-range), select-all/deselect, prop threading for export state, checkbox/keyboard interactions, and export button wiring.
Onboarding & Feature Badge
src/hooks/useNewFeatureBadge.ts, src/hooks/index.ts, src/pages/panel/components/OnboardingSystem.tsx, src/pages/panel/components/WelcomeOnboardingModal.tsx
New useNewFeatureBadge hook (re-exported); onboarding accepts featuresToAcknowledge and acknowledges features on dismiss; welcome modal adds "Timeline Export" card.
Store Extensions & Tests
src/stores/configStore.ts, src/stores/configStore.test.ts, src/stores/tabStore.test.ts
Config store gains exportSections and acknowledgedFeatures, actions setExportSections, acknowledgeFeature, isFeatureAcknowledged, selector selectExportSections, migrations/version bump; tests and storage mocks updated.
Timeout Ref Management Refactor
src/components/ThemedJsonView.tsx, src/pages/options/Options.tsx, src/pages/options/components/ResetButton.tsx, src/pages/panel/components/detail/primitives/PropertyRow/PropertyRow.tsx
Replaced inline timeouts with useRef-stored timer IDs and added unmount cleanup to avoid stale timers resetting state after unmount.
Tests & Test Setup
src/test/setup.ts
Removed conditional dynamic import; now unconditionally imports cleanup from @testing-library/react and runs afterEach(() => cleanup()).
Version Bump
package.json
Package version updated from 1.0.0 to 1.1.0.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I nibbled a checkbox, hopped through the list,
A badge gave a wink—oh, export exists!
Timers tucked safe in a tidy old ref,
Markdown and JSON leap out from the clef,
Hooray for small features that make devs say "neat!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat-export-timeline' accurately describes the main feature addition but uses a hyphenated slug format rather than a clear, readable sentence describing the change.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-export-timeline

Comment @coderabbitai help to get the list of available commands and usage tips.

@agch-dev
Copy link
Copy Markdown
Owner Author

@coderabbitai help

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
    • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
    • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve merge conflict to automatically resolve merge conflicts.
  • @coderabbitai autofix to automatically fix issues identified in unresolved review comments.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit configuration file (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

@agch-dev
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 | 🔴 Critical

Chain 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 get acknowledgedFeatures. Older installs miss later fields for the same reason. isFeatureAcknowledged() then reads .includes() from undefined, 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 | 🟠 Major

Keep export state independent from active filters, and make ordering explicit.

selectedExportIds survives search/hide changes, but both selectedEvents and the toolbar count are derived from filteredEvents. That means a selected event disappears from the export as soon as it is filtered out, and the export button can drop to 0/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 exportSections and acknowledgedFeatures are 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 capturedAt locally 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4b81d12 and 7481108.

📒 Files selected for processing (26)
  • package.json
  • src/components/ThemedJsonView.tsx
  • src/hooks/index.ts
  • src/hooks/useNewFeatureBadge.ts
  • src/lib/exportFormatter.test.ts
  • src/lib/exportFormatter.ts
  • src/lib/index.ts
  • src/pages/options/Options.tsx
  • src/pages/options/components/ResetButton.tsx
  • src/pages/panel/Panel.tsx
  • src/pages/panel/components/ActionButtons.tsx
  • src/pages/panel/components/EventList/EventList.tsx
  • src/pages/panel/components/EventRow.tsx
  • src/pages/panel/components/EventRowHeader.tsx
  • src/pages/panel/components/ExportModal.tsx
  • src/pages/panel/components/ExportModalSectionToggle.tsx
  • src/pages/panel/components/ExportToolbar.tsx
  • src/pages/panel/components/Header.tsx
  • src/pages/panel/components/OnboardingSystem.tsx
  • src/pages/panel/components/WelcomeOnboardingModal.tsx
  • src/pages/panel/components/detail/primitives/PropertyRow/PropertyRow.tsx
  • src/pages/panel/components/index.ts
  • src/stores/configStore.test.ts
  • src/stores/configStore.ts
  • src/stores/tabStore.test.ts
  • src/test/setup.ts

Comment thread src/lib/exportFormatter.test.ts
Comment thread src/lib/exportFormatter.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 | 🟠 Major

Run 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—so togglePin('traits', ...) can still hit undefined after 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) and handleDismiss() 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 using role="radiogroup" for mode selector.

The two mode buttons function as mutually exclusive options. While aria-pressed works, semantic role="radiogroup" with role="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

📥 Commits

Reviewing files that changed from the base of the PR and between 4b81d12 and 7481108.

📒 Files selected for processing (26)
  • package.json
  • src/components/ThemedJsonView.tsx
  • src/hooks/index.ts
  • src/hooks/useNewFeatureBadge.ts
  • src/lib/exportFormatter.test.ts
  • src/lib/exportFormatter.ts
  • src/lib/index.ts
  • src/pages/options/Options.tsx
  • src/pages/options/components/ResetButton.tsx
  • src/pages/panel/Panel.tsx
  • src/pages/panel/components/ActionButtons.tsx
  • src/pages/panel/components/EventList/EventList.tsx
  • src/pages/panel/components/EventRow.tsx
  • src/pages/panel/components/EventRowHeader.tsx
  • src/pages/panel/components/ExportModal.tsx
  • src/pages/panel/components/ExportModalSectionToggle.tsx
  • src/pages/panel/components/ExportToolbar.tsx
  • src/pages/panel/components/Header.tsx
  • src/pages/panel/components/OnboardingSystem.tsx
  • src/pages/panel/components/WelcomeOnboardingModal.tsx
  • src/pages/panel/components/detail/primitives/PropertyRow/PropertyRow.tsx
  • src/pages/panel/components/index.ts
  • src/stores/configStore.test.ts
  • src/stores/configStore.ts
  • src/stores/tabStore.test.ts
  • src/test/setup.ts

Comment thread src/lib/exportFormatter.ts
@agch-dev agch-dev merged commit b846664 into main Apr 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants