Skip to content

feat(app): add Share button with compact shareable links#2665

Draft
teeohhem wants to merge 1 commit into
mainfrom
tom/add-share-url-button
Draft

feat(app): add Share button with compact shareable links#2665
teeohhem wants to merge 1 commit into
mainfrom
tom/add-share-url-button

Conversation

@teeohhem

Copy link
Copy Markdown
Contributor

Summary

Adds a Share button — on search, Chart Explorer, dashboards, and the row/session side panels — that copies a shareable link to the clipboard. HyperDX serializes the entire view state into the URL, which produces very long, double-percent-encoded links; this button collapses large state into a single compressed ?share= token (using the browser-native CompressionStream — raw DEFLATE + base64url, so no new dependency), while small path-identified views (e.g. a dashboard) stay a plain URL so links are never made longer. A global hook in _app expands ?share= tokens back into normal query params on load, so every existing useQueryState binding works unchanged and old long links keep working. It's covered by unit tests (encode/decode round-trip, URL-safety, compressed-vs-plain selection, empty/duplicate-param cleaning) and five end-to-end Playwright tests spanning all surfaces, plus a changeset.

Screenshots or video

New "Share" button in the search / Chart Explorer / dashboard toolbars and in the row/session side-panel headers; it copies a link and shows a "Copied shareable link to clipboard" toast. (No before state — new feature. Static screenshots not captured in this run; behavior is exercised by the preview steps below and the E2E suite.)

How to test on Vercel preview

Preview routes: /search, /chart

Steps:

  1. Open /search and wait for the results table to load.
  2. Click the Share button (data-testid="share-link-button").
  3. Verify a notification reading "Copied shareable link to clipboard" appears.
  4. Open /chart and pick a source in the source picker (data-testid="source-selector").
  5. Click the Share button (data-testid="share-link-button").
  6. Confirm a "Copied shareable link to clipboard" notification appears.

References

  • Linear Issue:
  • Related PRs:

Adds a Share button on search, Chart Explorer, dashboards, and the row/session side panels that copies a shareable link to the clipboard. Large view state (e.g. Chart Explorer) is compressed into a single `?share=` token via the browser-native CompressionStream (deflate-raw + base64url) so links are far shorter than the raw query-param URL; small links (path-identified dashboards) stay plain. A global hook expands `?share=` tokens back to normal params on load. Includes unit tests, a changeset, and end-to-end Playwright coverage.
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jul 16, 2026 7:16pm
hyperdx-storybook Ready Ready Preview, Comment Jul 16, 2026 7:16pm

Request Review

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Jul 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 438 production lines changed (Tier 2 max: < 250)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 10
  • Production lines changed: 438 (+ 375 in test files, excluded from tier calculation)
  • Branch: tom/add-share-url-button
  • Author: teeohhem

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

No P0/P1 ship-blockers: the feature adds no server-side surface, pathname for the expanded URL is always taken from the victim's own window.location (no open-redirect), decoded params reach only query state the user could already set by hand (no new injection/privilege boundary), and the happy-path round-trip is exercised by unit and E2E tests.

🟡 P2 -- recommended

  • packages/app/src/utils/shareLink.ts:28 -- readAll accumulates decompressed chunks with no size cap and decodeShareToken bounds neither token length nor output, so an attacker-crafted ?share= token (deflate-raw allows ~1000:1 expansion) can inflate to hundreds of MB and freeze or OOM a victim's tab the moment the link is opened.
    • Fix: Reject oversized tokens in decodeShareToken and abort readAll (via reader.cancel()) once accumulated bytes exceed a fixed cap.
    • security-reviewer, correctness-reviewer
  • packages/app/src/hooks/useExpandShareLink.ts:34 -- expansion decompresses and router.replaces asynchronously after mount, so page components first read the URL that still contains only ?share=<token> (no real params) and render/fetch from defaults before the expanded params land, causing a flash of default state and a potential wasted initial query on ?share= opens.
    • Fix: Gate the page's initial render/query on share-token resolution, or expand the token synchronously before the first param read.
  • packages/app/src/SessionSidePanel.tsx:151, packages/app/src/components/DBRowSidePanel.tsx:136 -- both side-panel share handlers call buildShareUrl(window.location.search) directly without freezeTimeRange, so a link shared while a relative range (tq/isLive) is active re-evaluates against the recipient's clock and shows a different window than the sharer saw -- contradicting the freeze behavior the other three surfaces implement.
    • Fix: Route the side-panel share strings through freezeTimeRange with the panel's resolved time range, as the search/chart/dashboard surfaces do.
  • packages/app/src/hooks/useExpandShareLink.ts:31 -- the new expansion hook has no unit coverage for its non-happy paths (invalid/corrupt token notification, the strip-and-replace fallback, sibling-param handling); only shareLink.ts pure functions and the E2E happy path are tested.
    • Fix: Add unit tests for the invalid-token notification path and the token-expansion replace behavior.
    • testing-reviewer
🔵 P3 nitpicks (4)
  • packages/app/src/components/ShareLinkButton.tsx:46 -- the "Copied!" reset setTimeout is never cleared, so unmounting within the 2s window schedules a setState on an unmounted component (same pattern in DBRowSidePanel.tsx).
    • Fix: Clear the timeout on unmount with a ref and useEffect cleanup.
  • packages/app/src/components/ShareLinkButton.tsx:63 -- {...buttonProps} is spread after onClick, data-testid, and style, letting any caller silently override the share handler or test id.
    • Fix: Spread buttonProps first, then apply the fixed onClick/data-testid/style after it.
  • packages/app/src/hooks/useExpandShareLink.ts:43 -- the invalid-token branch does router.replace(window.location.pathname), discarding every query param, while its comment says it renders "from whatever real params remain."
    • Fix: Strip only the share param on the invalid path, or correct the comment to match the strip-everything behavior.
  • packages/app/src/DBSearchPage.tsx:1116 -- the getShareSearch = freezeTimeRange(window.location.search, searchedTimeRange) callback is duplicated verbatim across DBSearchPage, DBChartPage, and DBDashboardPage, and the two side panels re-implement the copy-and-toast handler instead of reusing ShareLinkButton.
    • Fix: Extract a shared useShareSearch hook and consolidate the side-panel handlers onto the shared component.

Reviewers (11): correctness, testing, maintainability, project-standards, agent-native, learnings, security, reliability, adversarial, kieran-typescript, julik-frontend-races.

Testing gaps: No test exercises a maliciously high-ratio deflate token to confirm the decode path is memory-bounded; the useExpandShareLink invalid-token and expansion branches are unit-tested only indirectly via E2E.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a Share button to the Search, Chart Explorer, Dashboard, and row/session side panels that copies a compact, shareable URL to the clipboard. Large view state (e.g. Chart Explorer's config blob) is compressed with the browser-native CompressionStream (raw DEFLATE + base64url) into a single ?share= token; small URLs stay plain so links are never lengthened. A global useExpandShareLink hook in _app.tsx decodes ?share= tokens back to normal query params on page load, leaving all existing useQueryState bindings untouched.

  • shareLink.ts encodes/decodes tokens, cleans duplicate/empty params, and freezes time ranges to absolute from/to values — used correctly by the Search, Chart, and Dashboard buttons.
  • SessionSidePanel and DBRowSidePanel call buildShareUrl(window.location.search) directly without going through freezeTimeRange, so a relative time param like tq=Past+1h survives into the shared link and will resolve to a different window for the recipient.
  • The useExpandShareLink hook marks itself handled at first token detection and never resets, so a second ?share= URL reached via client-side navigation is silently ignored.

Confidence Score: 4/5

Safe to merge with minor follow-up recommended for the two side panels.

The core compression/decompression pipeline, URL building, and expansion hook are well-designed and covered by unit and E2E tests. The two side panels (session and row) omit the freezeTimeRange step that all other Share surfaces apply, so shared links from those panels can embed a relative time window that resolves to a different data set for the recipient.

SessionSidePanel.tsx and components/DBRowSidePanel.tsx — both call buildShareUrl without first calling freezeTimeRange, leaving relative time params in the shared URL.

Important Files Changed

Filename Overview
packages/app/src/utils/shareLink.ts New core module: compress/decompress query-string tokens via CompressionStream+base64url, clean params, build and freeze share URLs; logic is well-tested and handles fallbacks correctly.
packages/app/src/hooks/useExpandShareLink.ts New hook that expands ?share= tokens on page load; works correctly for fresh page loads but the handledRef guard silently skips re-expansion for client-side navigations to a second share URL.
packages/app/src/components/ShareLinkButton.tsx New reusable Share button component; correctly delegates URL construction to the caller's getShareSearch callback and handles clipboard errors and copied state.
packages/app/src/SessionSidePanel.tsx Updated shareSession to use buildShareUrl, but omits freezeTimeRange — relative time params like tq=Past+1h survive into the share link, unlike the Search/Chart/Dashboard surfaces.
packages/app/src/components/DBRowSidePanel.tsx Replaced Mantine CopyButton with async buildShareUrl; same missing freezeTimeRange issue as SessionSidePanel — time range is passed through un-frozen from window.location.search.
packages/app/src/utils/tests/shareLink.test.ts Comprehensive unit tests covering encode/decode round-trip, URL safety, compression threshold, leading-? stripping, empty/duplicate param cleaning, and freezeTimeRange semantics.
packages/app/tests/e2e/features/share/share.spec.ts Five E2E tests covering Search, Dashboard, Chart Explorer, row side panel, and session side panel share flows; correctly grants clipboard permissions and validates both compressed and plain URL shapes.
packages/app/pages/_app.tsx Adds useExpandShareLink() call in AppContent; placing it at the _app level means it runs on every page without per-page wiring.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant ShareBtn as ShareLinkButton / panel
    participant SL as shareLink.ts
    participant Clip as Clipboard

    User->>ShareBtn: click Share
    ShareBtn->>SL: getShareSearch() freezeTimeRange(search, timeRange)
    ShareBtn->>SL: buildShareUrl(search)
    SL->>SL: cleanSearch drop empty/dup params
    alt query large enough to benefit
        SL->>SL: encodeShareToken CompressionStream base64url
        SL-->>ShareBtn: "origin/path?share=token"
    else query small
        SL-->>ShareBtn: origin/path?query
    end
    ShareBtn->>Clip: copyTextToClipboard(url)
    ShareBtn-->>User: Copied shareable link toast

    Note over User: Recipient opens link

    participant App as _app.tsx AppContent
    participant Hook as useExpandShareLink
    participant Router as Next.js router

    App->>Hook: useExpandShareLink() on mount
    Hook->>Hook: read share param from window.location.search
    alt token present
        Hook->>SL: decodeShareToken(token)
        SL->>SL: DecompressionStream deflate-raw
        SL-->>Hook: expanded query string
        Hook->>Router: router.replace(path?expanded, shallow)
        Router-->>App: nuqs hooks re-read normal params
    else no token
        Hook-->>App: no-op
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant ShareBtn as ShareLinkButton / panel
    participant SL as shareLink.ts
    participant Clip as Clipboard

    User->>ShareBtn: click Share
    ShareBtn->>SL: getShareSearch() freezeTimeRange(search, timeRange)
    ShareBtn->>SL: buildShareUrl(search)
    SL->>SL: cleanSearch drop empty/dup params
    alt query large enough to benefit
        SL->>SL: encodeShareToken CompressionStream base64url
        SL-->>ShareBtn: "origin/path?share=token"
    else query small
        SL-->>ShareBtn: origin/path?query
    end
    ShareBtn->>Clip: copyTextToClipboard(url)
    ShareBtn-->>User: Copied shareable link toast

    Note over User: Recipient opens link

    participant App as _app.tsx AppContent
    participant Hook as useExpandShareLink
    participant Router as Next.js router

    App->>Hook: useExpandShareLink() on mount
    Hook->>Hook: read share param from window.location.search
    alt token present
        Hook->>SL: decodeShareToken(token)
        SL->>SL: DecompressionStream deflate-raw
        SL-->>Hook: expanded query string
        Hook->>Router: router.replace(path?expanded, shallow)
        Router-->>App: nuqs hooks re-read normal params
    else no token
        Hook-->>App: no-op
    end
Loading

Comments Outside Diff (1)

  1. packages/app/src/SessionSidePanel.tsx, line 150-158 (link)

    P2 Missing time-range freeze for shared session links

    shareSession calls buildShareUrl(window.location.search) directly, so any relative time range in the URL (e.g. tq=Past+1h) is included verbatim in the shared link. A recipient who opens the link an hour later will land on a different "Past 1h" window and the targeted session may not appear in the filtered list. Every other Share surface (Search, Chart, Dashboard) calls freezeTimeRange first to pin from/to and drop tq — this panel is the only one that skips that step.

    Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

Fix All in Claude Code Fix All in Conductor Fix All in Cursor Fix All in Codex

Reviews (1): Last reviewed commit: "feat(app): add Share button with compact..." | Re-trigger Greptile

Comment on lines +134 to +144
const handleShareCopy = useCallback(async () => {
const ok = await copyTextToClipboard(
await buildShareUrl(window.location.search),
);
if (!ok) {
notifications.show({ color: 'red', message: CLIPBOARD_ERROR_MESSAGE });
return;
}
setShareCopied(true);
setTimeout(() => setShareCopied(false), 2000);
}, []);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Row side panel also skips freezeTimeRange

Like SessionSidePanel, handleShareCopy passes window.location.search directly to buildShareUrl without first calling freezeTimeRange. If the current URL carries a relative time query (tq=...), the shared link will re-evaluate that range against the recipient's clock. The Search, Chart, and Dashboard share buttons all pin absolute from/to and strip tq before building the URL; this panel should do the same for consistency.

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

Comment on lines +22 to +58
useEffect(() => {
if (handledRef.current) {
return;
}

const token = new URLSearchParams(window.location.search).get(SHARE_PARAM);
if (!token) {
return;
}
handledRef.current = true;

let cancelled = false;
void (async () => {
const expanded = await decodeShareToken(token);
if (cancelled) {
return;
}
if (expanded == null) {
notifications.show({
color: 'red',
message: 'This shared link is invalid or corrupted.',
});
// Strip the bad token; render from whatever real params remain.
router.replace(window.location.pathname, undefined, { shallow: true });
return;
}
router.replace(`${window.location.pathname}?${expanded}`, undefined, {
shallow: true,
});
})();

return () => {
cancelled = true;
};
// Run once on mount: window.location is the source of truth for the token.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 handledRef blocks client-side re-expansion across page transitions

handledRef.current is set to true the moment a token is detected and never reset. AppContent (where this hook lives) is mounted once for the lifetime of the Next.js app and is not torn down on client-side navigation. If a user has the app open, navigates to a second ?share=… URL via router.push or a <Link> click (without a full page reload), the effect's early-return guard fires and the second token is never expanded. Real-world impact is low because share links are normally opened in a new tab, but the behavior is surprising if anyone creates an in-app link to a share URL.

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

@github-actions

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 245 passed • 1 skipped • 1064s

Status Count
✅ Passed 245
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant