Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flatten ?filters=(...)&labels=(...) to ?f=...&f=...&l=...&l=... #4810

Merged
merged 6 commits into from
Jan 16, 2025

Conversation

apata
Copy link
Contributor

@apata apata commented Nov 12, 2024

Changes

  • Moves current URL params parsing logic to legacy-jsonurl-url-search-params.ts
  • Adds custom serialization logic for labels and filters in url-search-params.ts and encodeURIComponent based logic for other simple params.
  • Adds redirect logic from jsonurl search params to new search params.

Tests

  • Automated tests have been added

Changelog

  • Entry has been added to changelog

Documentation

  • This change does not need a documentation update

Dark mode

  • This PR does not change the UI

@apata apata added the preview label Nov 12, 2024
Copy link

Preview environment👷🏼‍♀️🏗️
PR-4810

@ruslandoga
Copy link
Contributor

ruslandoga commented Nov 20, 2024

👋

Sorry for a drive-by comment on a draft PR, but if you set the attribute key to f[]= then it would get automatically decoded into a list in Plug / Phoenix. Otherwise a custom decoder for query strings would need to be implemented.

More info: https://hexdocs.pm/plug/Plug.Conn.Query.html

@apata apata changed the title WIP: flatten ?filters=(...) to ?f=...&f=... [WIP] Flatten ?filters=(...) to ?f=...&f=... Dec 2, 2024
@apata apata force-pushed the fix-links-by-flattening branch from 0f16e70 to caf00fe Compare December 11, 2024 15:07
@apata
Copy link
Contributor Author

apata commented Dec 11, 2024

👋

Sorry for a drive-by comment on a draft PR, but if you set the attribute key to f[]= then it would get automatically decoded into a list in Plug / Phoenix. Otherwise a custom decoder for query strings would need to be implemented.

More info: https://hexdocs.pm/plug/Plug.Conn.Query.html

Thanks for bringing it up @ruslandoga ! At the moment, this format must be recognizable only to the FE and ideally humans, though. I think of it as URL-stored dashboard state.

Currently, the BE receives applied dashboard filters in URL-encoded stringified JSON, and there's an extra transform taking place https://github.com/plausible/analytics/blob/master/assets/js/dashboard/util/filters.js#L231 before that, which we're not planning to get rid of (in the interest of readable URLs).

@apata apata force-pushed the fix-links-by-flattening branch 2 times, most recently from 6f83914 to cd6b053 Compare December 11, 2024 16:20
@apata apata changed the title [WIP] Flatten ?filters=(...) to ?f=...&f=... Flatten ?filters=(...)&labels=(...) to ?f=...&f=...&l=...&l=... Dec 11, 2024
@ukutaht
Copy link
Contributor

ukutaht commented Jan 2, 2025

The idea with the new filters format was to also support nesting in case we need it in the future. Stuff like:

[
  ["or", [
    ["and", [
      ["is", "visit:source",  "Google", {case_sensitive: false}],
      ["is", "visit:utm_medium", "is", "cpc"]
    ]],
    ["and", [
      ["is", "visit:source", "Facebook", {case_sensitive: false}],
      ["is", "visit:utm_medium", "paid_social"]
    ]]
  ]]
]

This format seems to make that a bit harder. Did you consider that as well @apata ?

@apata
Copy link
Contributor Author

apata commented Jan 6, 2025

Good question, @ukutaht! I expect there to be a bunch of ways to do this because we know the key words: and, or, not, all operators and all operator options.

[
  [
    "or",
    [
      [
        "and",
        [
          [
            "is",
            "visit:source",
            ["Google", "Bing"],
            { "case_sensitive": false }
          ],
          ["is", "visit:utm_medium", "is", ["cpc"]]
        ]
      ],
      [
        "and",
        [
          ["is", "visit:source", ["Facebook"], { "case_sensitive": false }],
          ["is", "visit:utm_medium", ["paid_social"]]
        ]
      ]
    ]
  ],
  ["not", ["is", "visit:country", ["Estonia"]]],
  ["contains", "event:goal", ["trial"], { "case_sensitive": false }]
]

For this edgy filter tree, an URL in the new style might be

?f=or/2&f=and/2&f=is/i,source,Google,Bing&f=is,utm_medium,cpc&f=and/2&f=is/i,source,Facebook&f=is,utm_medium,paid_social&f=not/1&f=is,country,Estonia&f=contains/i,goal,trial

This would be reconstructable thanks to fixed order of f= in the URL and known group lengths.

// url.split("&").map(i => i.split("=")).filter(i => i[0] === "f").map(i => i[1]) -->
[
  "or/2",
  "and/2",
  "is/i,source,Google,Bing",
  "is,utm_medium,cpc",
  "and/2",
  "is/i,source,Facebook",
  "is,utm_medium,paid_social",
  "not/1",
  "is,country,Estonia",
  "contains/i,goal,trial",
]
// reconstruct recursively

@ukutaht
Copy link
Contributor

ukutaht commented Jan 7, 2025

Cool - the format makes sense to me. While it does diverge a bit form what we initially designed, and comes with the annoyance of managing yet another backwards compatibility redirect, I think it's probably worth it in order to have URLs that can be copy-pasted easily.

@ukutaht
Copy link
Contributor

ukutaht commented Jan 7, 2025

Would be good to get another review/opinion - @aerosol @zoldar @macobo thoughts on this?

const serializedFilter = [
encodeURIComponentPermissive(operator, ':/'),
encodeURIComponentPermissive(dimension, ':/'),
...clauses.map((clause) =>
Copy link
Contributor

@macobo macobo Jan 9, 2025

Choose a reason for hiding this comment

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

We recently added support for case insensitivity in APIv2 via modifiers (plausible/docs#567).
While we haven't exposed this in the frontend, it's something we should do once we figure out the UX. I'm not sure whether this function is extendable to allow for that structure though.

Note that in general modifiers are not always flags and may contain arbitrary data structures within.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe it is: #4810 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that in general modifiers are not always flags and may contain arbitrary data structures within.

The filter modifiers and structures we support is up to us, so I'm not clear on what you mean by arbitrary.

That said, I can imagine something like the following working:

-> `is\non_flag_modifier_name:${encodeURIComponent(JSON.stringify(complex_structure))},<dimension>,<clause1>,<clause2>`

That's stretching the limits of readability though.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not clear on what you mean by arbitrary.

@macobo and I were just bouncing around some ideas about how to extend filtering capabilities in the future which would utilize more modifiers. Note that these new modifiers will not necessarily be built in the short term but having the ability to extend filters with this kind of stuff in the future would be really useful.

Arbitrary date range example:
Screenshot 2025-01-09 at 18 24 13

More flags:
Screenshot 2025-01-09 at 18 24 22

See this discussion: https://3.basecamp.com/5308029/buckets/26383192/card_tables/cards/7793417773

As we were discussing it did cross my mind that this may not work well with the proposed URL structure here.,,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the extra examples! What makes these modifiers arbitrary?

We know what keys to look for, and even what the possible values can be.

As I see it, filter clauses are the only arbitrary value currently: their values are unpredictable to us, and may overlap with any of our option names or option configurations.

That's why in this solution, they are expanded to be the last members of the comma separated array: f=<known word at known position>/<optional known words and values>,<known word at known position>,...unpredictable words...

However, from my side, I'm not married to this solution. There's various others that are totally unexplored at the moment, like enriching jsonurl etc. It's just that "working well" will be hard to solve for. Short, easy to implement, linkable, readable are some of the things that I optimized for. I personally think expandable is also one of the qualities, but I see that doubt remains.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think something analogous to what you proposed in #4810 (comment) should work for the original question (how to handle more complex data structures in filter modifiers and should unblock us for now.

@apata apata force-pushed the fix-links-by-flattening branch from c4e5e89 to 7df16a1 Compare January 9, 2025 13:02
Copy link
Contributor

@macobo macobo left a comment

Choose a reason for hiding this comment

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

Behaviorally this is great and I appreciate the extensive tests!

I didn't do extensive testing on the backwards redirect.

I also had some minor suggestions around code structure to avoid the backwards redirects causing issues in the future. Let me know if you'd prefer me to push to the branch to implement the suggestions.

) {
const searchRecord = parseSearch(windowLocation.search)
const getValue = (k: string) => searchRecord[k]
export function maybeGetRedirectTargetFromLegacySearchParams(
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add a comment detailing the history of this above it - this will help future readers of the code.

Suggested change
export function maybeGetRedirectTargetFromLegacySearchParams(
/*
Calculates whether to redirect from legacy url format(s) to new formats.
We serialize dashboard filters in the URL and maintain old URLs for backwards compatibility reasons to not break old links.
There have been two previous formats:
1. legacy-searchparams - a custom encoding schema was used for filters. This was deemed not flexible enough and diverged from how we represented filters in the code.
2. legacy-jsonurl - jsonurl was used to encode the more complex filter structure via json url. However the links generated via this didn't always work across all platforms (e.g. Twitter).
*/
export function maybeGetRedirectTargetFromLegacySearchParams(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea. Added with slight modifications.

const getValue = (k: string) => searchRecord[k]
export function maybeGetRedirectTargetFromLegacySearchParams(
windowLocation: Location
): null | string {
Copy link
Contributor

Choose a reason for hiding this comment

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

I was struggling reviewing this function. I think it's because the original 'legacy' format and the new 'legacy' format are both called legacy in this, and the code is structured differently, causing friction.

I suggest:

  1. naming the formats: legacy-jsonurl and legacy-searchparams
  2. moving parseLegacyFilter and parseLegacyPropsFilter and parseLegacyFilter to legacy-searchparams.ts
  3. Splitting this function into two parts, function function redirectTarget() and putting them in legacy-searchparams.ts and legacy-jsonurl-....

I think this would increase consistency and simplify keeping this working if/as we iterate this further.

Copy link
Contributor Author

@apata apata Jan 14, 2025

Choose a reason for hiding this comment

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

I agree that what you propose is better, but since this the first redirect wasn't properly tested, I opted against it originally: in review, any slight breaking changes could have slipped past. I've now split it out though.

@@ -0,0 +1,80 @@
/* @format */
Copy link
Contributor

Choose a reason for hiding this comment

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

I am generally not a fan of utils - they tend to become grab bags of misc code. Given the behavior becomes also more integrated with the rest of the app code, I think some thought is warranted.

How would you feel about moving `url-search-params and the legacy files under assets/js/dashboard/navigation/ or creating a new folder for them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right that utils can get messy, but I think I'd prefer it solved in a separate refactor that tackles code organization only.

There's relations between url, api, url search params, query, filters that our current code organization isn't fully capturing. Just now I had to solve some circular import dependency issue.

@apata apata force-pushed the fix-links-by-flattening branch from a85f8e6 to 33dc1fd Compare January 14, 2025 10:21
@apata apata force-pushed the fix-links-by-flattening branch from 33dc1fd to c20fbd3 Compare January 14, 2025 10:22
@apata apata added this pull request to the merge queue Jan 16, 2025
Merged via the queue into master with commit 90a2c5d Jan 16, 2025
8 checks passed
@apata apata deleted the fix-links-by-flattening branch January 23, 2025 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants