Skip to content

fix(dashboards): handle kea-router auto-parsed query_variables in URL params#53828

Open
lawrence3699 wants to merge 1 commit intoPostHog:masterfrom
lawrence3699:fix/dashboard-query-variables-url-parsing
Open

fix(dashboards): handle kea-router auto-parsed query_variables in URL params#53828
lawrence3699 wants to merge 1 commit intoPostHog:masterfrom
lawrence3699:fix/dashboard-query-variables-url-parsing

Conversation

@lawrence3699
Copy link
Copy Markdown

Problem

Dashboard query_variables URL parameter is silently ignored when manually constructing a URL without a trailing %20.

Works: ?query_variables={"card_name":"Polukranos, Unchained"}%20
Fails: ?query_variables={"card_name":"Polukranos, Unchained"}

Root cause

kea-router's parseValue() has two branches for JSON-like strings:

  • {...} (no trailing space) → auto-parses via JSON.parse() → returns an object
  • {...} (trailing space) → strips the space → returns a string

parseURLVariables() unconditionally calls JSON.parse() on the search param value. When kea-router already parsed it into an object, JSON.parse(someObject) stringifies it to "[object Object]" and throws a SyntaxError, which the catch block swallows — silently dropping all query variables.

The trailing %20 workaround prevents kea-router from auto-parsing the JSON, so the value stays as a string and JSON.parse() succeeds.

Fix

Check typeof raw === 'string' before calling JSON.parse(). If the value is already an object (auto-parsed by kea-router), use it directly. Applied the same fix to parseURLFilters() which has the same latent bug.

Changes

  • frontend/src/scenes/dashboard/dashboardUtils.ts — guard parseURLVariables and parseURLFilters against pre-parsed object values
  • frontend/src/scenes/dashboard/dashboardUtils.test.ts — new unit tests covering both string and object inputs

How I verified

  • All 7 new unit tests pass
  • All 124 existing dashboard tests pass (dashboardLogic.test.ts, dashboardQuickFiltersDebounce.test.ts, etc.)

Closes #53788

… params

kea-router's parseValue() auto-parses JSON-like URL parameter values
into objects when there is no trailing whitespace. parseURLVariables()
and parseURLFilters() unconditionally called JSON.parse() on the value,
which fails silently when the value is already an object — causing
dashboard query_variables to be ignored on initial page load.

The fix checks typeof before parsing: if the value is already an object
(auto-parsed by kea-router), use it directly.

Closes PostHog#53788
Copilot AI review requested due to automatic review settings April 9, 2026 09:00
@assign-reviewers-posthog assign-reviewers-posthog bot requested a review from a team April 9, 2026 09:00
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 9, 2026

Vulnerabilities

No security concerns identified. The raw value coming from URL params is treated only as a data object after parsing — it is never executed, evaluated, or used in a privileged context.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: frontend/src/scenes/dashboard/dashboardUtils.test.ts
Line: 8-35

Comment:
**Prefer parameterised tests for the string-vs-object cases**

The two happy-path tests in each `describe` block (string input and already-parsed object input) exercise the same function with different input types and can be collapsed into a single `it.each`. The custom instructions ask for parameterised tests, and this pattern is repeated for both `parseURLVariables` and `parseURLFilters`.

```suggestion
describe('parseURLVariables', () => {
    it.each([
        ['a JSON string value', '{"card_name":"Polukranos, Unchained"}', { card_name: 'Polukranos, Unchained' }],
        [
            'an already-parsed object (kea-router auto-parse)',
            { card_name: 'Polukranos, Unchained' },
            { card_name: 'Polukranos, Unchained' },
        ],
    ])('parses %s from search params', (_, input, expected) => {
        const result = parseURLVariables({ [SEARCH_PARAM_QUERY_VARIABLES_KEY]: input })
        expect(result).toEqual(expected)
    })

    it('returns empty object when key is missing', () => {
        expect(parseURLVariables({})).toEqual({})
    })

    it('returns empty object for invalid JSON string', () => {
        const searchParams = {
            [SEARCH_PARAM_QUERY_VARIABLES_KEY]: 'not-json',
        }
        expect(parseURLVariables(searchParams)).toEqual({})
    })
})
```

The same pattern applies to the `parseURLFilters` describe block.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: frontend/src/scenes/dashboard/dashboardUtils.test.ts
Line: 38-58

Comment:
**Missing invalid JSON test for `parseURLFilters`**

`parseURLVariables` includes an invalid-JSON test (line 31–35) to confirm the error path returns `{}`, but `parseURLFilters` does not. Since both functions share the same `try/catch` structure, coverage should be symmetric. Adding this case ensures the catch branch for `parseURLFilters` stays tested as the code evolves.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(dashboards): handle kea-router auto-..." | Re-trigger Greptile

Comment on lines +8 to +35
describe('parseURLVariables', () => {
it('parses a JSON string value from search params', () => {
const searchParams = {
[SEARCH_PARAM_QUERY_VARIABLES_KEY]: '{"card_name":"Polukranos, Unchained"}',
}
const result = parseURLVariables(searchParams)
expect(result).toEqual({ card_name: 'Polukranos, Unchained' })
})

it('handles an already-parsed object from search params (kea-router auto-parse)', () => {
// When the URL has no trailing %20, kea-router auto-parses JSON values into objects
const searchParams = {
[SEARCH_PARAM_QUERY_VARIABLES_KEY]: { card_name: 'Polukranos, Unchained' },
}
const result = parseURLVariables(searchParams)
expect(result).toEqual({ card_name: 'Polukranos, Unchained' })
})

it('returns empty object when key is missing', () => {
expect(parseURLVariables({})).toEqual({})
})

it('returns empty object for invalid JSON string', () => {
const searchParams = {
[SEARCH_PARAM_QUERY_VARIABLES_KEY]: 'not-json',
}
expect(parseURLVariables(searchParams)).toEqual({})
})
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 Prefer parameterised tests for the string-vs-object cases

The two happy-path tests in each describe block (string input and already-parsed object input) exercise the same function with different input types and can be collapsed into a single it.each. The custom instructions ask for parameterised tests, and this pattern is repeated for both parseURLVariables and parseURLFilters.

Suggested change
describe('parseURLVariables', () => {
it('parses a JSON string value from search params', () => {
const searchParams = {
[SEARCH_PARAM_QUERY_VARIABLES_KEY]: '{"card_name":"Polukranos, Unchained"}',
}
const result = parseURLVariables(searchParams)
expect(result).toEqual({ card_name: 'Polukranos, Unchained' })
})
it('handles an already-parsed object from search params (kea-router auto-parse)', () => {
// When the URL has no trailing %20, kea-router auto-parses JSON values into objects
const searchParams = {
[SEARCH_PARAM_QUERY_VARIABLES_KEY]: { card_name: 'Polukranos, Unchained' },
}
const result = parseURLVariables(searchParams)
expect(result).toEqual({ card_name: 'Polukranos, Unchained' })
})
it('returns empty object when key is missing', () => {
expect(parseURLVariables({})).toEqual({})
})
it('returns empty object for invalid JSON string', () => {
const searchParams = {
[SEARCH_PARAM_QUERY_VARIABLES_KEY]: 'not-json',
}
expect(parseURLVariables(searchParams)).toEqual({})
})
describe('parseURLVariables', () => {
it.each([
['a JSON string value', '{"card_name":"Polukranos, Unchained"}', { card_name: 'Polukranos, Unchained' }],
[
'an already-parsed object (kea-router auto-parse)',
{ card_name: 'Polukranos, Unchained' },
{ card_name: 'Polukranos, Unchained' },
],
])('parses %s from search params', (_, input, expected) => {
const result = parseURLVariables({ [SEARCH_PARAM_QUERY_VARIABLES_KEY]: input })
expect(result).toEqual(expected)
})
it('returns empty object when key is missing', () => {
expect(parseURLVariables({})).toEqual({})
})
it('returns empty object for invalid JSON string', () => {
const searchParams = {
[SEARCH_PARAM_QUERY_VARIABLES_KEY]: 'not-json',
}
expect(parseURLVariables(searchParams)).toEqual({})
})
})

The same pattern applies to the parseURLFilters describe block.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/scenes/dashboard/dashboardUtils.test.ts
Line: 8-35

Comment:
**Prefer parameterised tests for the string-vs-object cases**

The two happy-path tests in each `describe` block (string input and already-parsed object input) exercise the same function with different input types and can be collapsed into a single `it.each`. The custom instructions ask for parameterised tests, and this pattern is repeated for both `parseURLVariables` and `parseURLFilters`.

```suggestion
describe('parseURLVariables', () => {
    it.each([
        ['a JSON string value', '{"card_name":"Polukranos, Unchained"}', { card_name: 'Polukranos, Unchained' }],
        [
            'an already-parsed object (kea-router auto-parse)',
            { card_name: 'Polukranos, Unchained' },
            { card_name: 'Polukranos, Unchained' },
        ],
    ])('parses %s from search params', (_, input, expected) => {
        const result = parseURLVariables({ [SEARCH_PARAM_QUERY_VARIABLES_KEY]: input })
        expect(result).toEqual(expected)
    })

    it('returns empty object when key is missing', () => {
        expect(parseURLVariables({})).toEqual({})
    })

    it('returns empty object for invalid JSON string', () => {
        const searchParams = {
            [SEARCH_PARAM_QUERY_VARIABLES_KEY]: 'not-json',
        }
        expect(parseURLVariables(searchParams)).toEqual({})
    })
})
```

The same pattern applies to the `parseURLFilters` describe block.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +38 to +58
describe('parseURLFilters', () => {
it('parses a JSON string value from search params', () => {
const searchParams = {
[SEARCH_PARAM_FILTERS_KEY]: '{"date_from":"-7d"}',
}
const result = parseURLFilters(searchParams)
expect(result).toEqual({ date_from: '-7d' })
})

it('handles an already-parsed object from search params (kea-router auto-parse)', () => {
const searchParams = {
[SEARCH_PARAM_FILTERS_KEY]: { date_from: '-7d', date_to: 'now' },
}
const result = parseURLFilters(searchParams)
expect(result).toEqual({ date_from: '-7d', date_to: 'now' })
})

it('returns empty object when key is missing', () => {
expect(parseURLFilters({})).toEqual({})
})
})
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 Missing invalid JSON test for parseURLFilters

parseURLVariables includes an invalid-JSON test (line 31–35) to confirm the error path returns {}, but parseURLFilters does not. Since both functions share the same try/catch structure, coverage should be symmetric. Adding this case ensures the catch branch for parseURLFilters stays tested as the code evolves.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/scenes/dashboard/dashboardUtils.test.ts
Line: 38-58

Comment:
**Missing invalid JSON test for `parseURLFilters`**

`parseURLVariables` includes an invalid-JSON test (line 31–35) to confirm the error path returns `{}`, but `parseURLFilters` does not. Since both functions share the same `try/catch` structure, coverage should be symmetric. Adding this case ensures the catch branch for `parseURLFilters` stays tested as the code evolves.

How can I resolve this? If you propose a fix, please make it concise.

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

Fixes dashboard URL parsing so query_variables (and query_filters) work whether kea-router provides the param value as a JSON string or an already-parsed object (avoiding silent drops when the trailing %20 is missing).

Changes:

  • Guard parseURLVariables and parseURLFilters to only JSON.parse when the raw value is a string.
  • Add unit tests covering both string and already-parsed object inputs for variables/filters parsing.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
frontend/src/scenes/dashboard/dashboardUtils.ts Makes URL param parsing resilient to kea-router auto-parsing of JSON-like values.
frontend/src/scenes/dashboard/dashboardUtils.test.ts Adds regression tests for both string and object search param inputs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +30 to +35
it('returns empty object for invalid JSON string', () => {
const searchParams = {
[SEARCH_PARAM_QUERY_VARIABLES_KEY]: 'not-json',
}
expect(parseURLVariables(searchParams)).toEqual({})
})
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The "invalid JSON" test will trigger console.error inside parseURLVariables (since the function logs on parse failures). To keep test output clean and consistent with other tests in the repo, consider stubbing/spying on console.error in this test (optionally asserting the call) and restoring it afterward.

Copilot uses AI. Check for mistakes.
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.

Bug Report: Dashboard "query_variables" URL parameter is ignored unless an encoded space is included

2 participants