From 7b499b1bcc60dc138cdba9f269ddc9d3ba79e51f Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 27 Jan 2026 11:30:23 -0500 Subject: [PATCH 1/3] support search param in activation url pattern --- packages/client/src/clients/guide/client.ts | 5 +- packages/client/src/clients/guide/types.ts | 4 +- .../client/test/clients/guide/helpers.test.ts | 117 ++++++++++++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/packages/client/src/clients/guide/client.ts b/packages/client/src/clients/guide/client.ts index f3ee847e9..fdd25bcf4 100644 --- a/packages/client/src/clients/guide/client.ts +++ b/packages/client/src/clients/guide/client.ts @@ -975,7 +975,10 @@ export class KnockGuideClient { remoteGuide.activation_url_patterns.map((rule) => { return { ...rule, - pattern: new URLPattern({ pathname: rule.pathname }), + pattern: new URLPattern({ + pathname: rule.pathname ?? undefined, + search: rule.search ?? undefined, + }), }; }); diff --git a/packages/client/src/clients/guide/types.ts b/packages/client/src/clients/guide/types.ts index f7112713b..2b5fcd2b6 100644 --- a/packages/client/src/clients/guide/types.ts +++ b/packages/client/src/clients/guide/types.ts @@ -33,7 +33,9 @@ export interface GuideActivationUrlRuleData { interface GuideActivationUrlPatternData { directive: "allow" | "block"; - pathname: string; + // At least one part should be present. + pathname?: string; + search?: string; } export interface GuideData { diff --git a/packages/client/test/clients/guide/helpers.test.ts b/packages/client/test/clients/guide/helpers.test.ts index 4c8764529..78f584b95 100644 --- a/packages/client/test/clients/guide/helpers.test.ts +++ b/packages/client/test/clients/guide/helpers.test.ts @@ -540,4 +540,121 @@ describe("predicateUrlPatterns", () => { expect(predicateUrlPatterns(differentUrl, patterns)).toBe(true); }); }); + + describe("with search patterns", () => { + test("returns true when URL matches an allow pattern with search params", () => { + const patterns: KnockGuideActivationUrlPattern[] = [ + { + directive: "allow", + pattern: new URLPattern({ pathname: "/dashboard", search: "tab=settings" }), + }, + ]; + + const matchingUrl = new URL("https://example.com/dashboard?tab=settings"); + const nonMatchingUrl = new URL("https://example.com/dashboard?tab=overview"); + + expect(predicateUrlPatterns(matchingUrl, patterns)).toBe(true); + expect(predicateUrlPatterns(nonMatchingUrl, patterns)).toBe(undefined); + }); + + test("returns false when URL matches a block pattern with search params", () => { + const patterns: KnockGuideActivationUrlPattern[] = [ + { + directive: "block", + pattern: new URLPattern({ pathname: "/admin", search: "mode=debug" }), + }, + ]; + + const matchingUrl = new URL("https://example.com/admin?mode=debug"); + const nonMatchingUrl = new URL("https://example.com/admin?mode=normal"); + + expect(predicateUrlPatterns(matchingUrl, patterns)).toBe(false); + expect(predicateUrlPatterns(nonMatchingUrl, patterns)).toBe(true); + }); + + test("handles wildcard patterns in search params", () => { + const patterns: KnockGuideActivationUrlPattern[] = [ + { + directive: "allow", + pattern: new URLPattern({ pathname: "/page", search: "id=*" }), + }, + ]; + + const matchingUrl = new URL("https://example.com/page?id=123"); + const anotherMatchingUrl = new URL("https://example.com/page?id=abc"); + const nonMatchingUrl = new URL("https://example.com/page?other=value"); + + expect(predicateUrlPatterns(matchingUrl, patterns)).toBe(true); + expect(predicateUrlPatterns(anotherMatchingUrl, patterns)).toBe(true); + expect(predicateUrlPatterns(nonMatchingUrl, patterns)).toBe(undefined); + }); + + test("matches when pathname matches but no search pattern specified", () => { + const patterns: KnockGuideActivationUrlPattern[] = [ + { + directive: "allow", + pattern: new URLPattern({ pathname: "/dashboard" }), + }, + ]; + + // Should match regardless of search params when no search pattern specified + const urlWithSearch = new URL("https://example.com/dashboard?tab=settings"); + const urlWithoutSearch = new URL("https://example.com/dashboard"); + + expect(predicateUrlPatterns(urlWithSearch, patterns)).toBe(true); + expect(predicateUrlPatterns(urlWithoutSearch, patterns)).toBe(true); + }); + + test("block pattern with search takes precedence over allow pattern without search", () => { + const patterns: KnockGuideActivationUrlPattern[] = [ + { + directive: "allow", + pattern: new URLPattern({ pathname: "/settings/*" }), + }, + { + directive: "block", + pattern: new URLPattern({ pathname: "/settings/admin", search: "dangerous=true" }), + }, + ]; + + const blockedUrl = new URL("https://example.com/settings/admin?dangerous=true"); + const allowedUrl = new URL("https://example.com/settings/admin?dangerous=false"); + + expect(predicateUrlPatterns(blockedUrl, patterns)).toBe(false); + expect(predicateUrlPatterns(allowedUrl, patterns)).toBe(true); + }); + + test("handles multiple search params in pattern", () => { + const patterns: KnockGuideActivationUrlPattern[] = [ + { + directive: "allow", + pattern: new URLPattern({ pathname: "/report", search: "type=sales&year=2024" }), + }, + ]; + + const matchingUrl = new URL("https://example.com/report?type=sales&year=2024"); + const partialMatchUrl = new URL("https://example.com/report?type=sales"); + const wrongOrderUrl = new URL("https://example.com/report?year=2024&type=sales"); + + expect(predicateUrlPatterns(matchingUrl, patterns)).toBe(true); + expect(predicateUrlPatterns(partialMatchUrl, patterns)).toBe(undefined); + // URLPattern is sensitive to search param order + expect(predicateUrlPatterns(wrongOrderUrl, patterns)).toBe(undefined); + }); + + test("handles search pattern with wildcard for any search params", () => { + const patterns: KnockGuideActivationUrlPattern[] = [ + { + directive: "block", + pattern: new URLPattern({ pathname: "/api", search: "*" }), + }, + ]; + + const urlWithSearch = new URL("https://example.com/api?key=value"); + const urlWithoutSearch = new URL("https://example.com/api"); + + expect(predicateUrlPatterns(urlWithSearch, patterns)).toBe(false); + expect(predicateUrlPatterns(urlWithoutSearch, patterns)).toBe(false); + }); + }); }); From 632e822600ef9f71c006894f49224958acf322cf Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 27 Jan 2026 17:20:16 -0500 Subject: [PATCH 2/3] add an additional test --- .../client/test/clients/guide/helpers.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/client/test/clients/guide/helpers.test.ts b/packages/client/test/clients/guide/helpers.test.ts index 78f584b95..ed9a875f6 100644 --- a/packages/client/test/clients/guide/helpers.test.ts +++ b/packages/client/test/clients/guide/helpers.test.ts @@ -642,6 +642,27 @@ describe("predicateUrlPatterns", () => { expect(predicateUrlPatterns(wrongOrderUrl, patterns)).toBe(undefined); }); + test("handles multiple search params in pattern, to match a single search param regardless of the order", () => { + const patterns: KnockGuideActivationUrlPattern[] = [ + { + directive: "allow", + pattern: new URLPattern({ pathname: "/report", search: "*role=admin*" }), + }, + ]; + + const url1 = new URL("https://example.com/report?role=admin"); + const url2 = new URL("https://example.com/report?year=2022&role=admin"); + const url3 = new URL("https://example.com/report?role=admin&year=2022"); + const url4 = new URL("https://example.com/report?location=nyc&role=admin&year=2022"); + const url5 = new URL("https://example.com/report?location=nyc&year=2022"); + + expect(predicateUrlPatterns(url1, patterns)).toBe(true); + expect(predicateUrlPatterns(url2, patterns)).toBe(true); + expect(predicateUrlPatterns(url3, patterns)).toBe(true); + expect(predicateUrlPatterns(url4, patterns)).toBe(true); + expect(predicateUrlPatterns(url5, patterns)).toBe(undefined); + }); + test("handles search pattern with wildcard for any search params", () => { const patterns: KnockGuideActivationUrlPattern[] = [ { From 1eeac186d3c9baffcf0c64b42b86a29bad352b1e Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 28 Jan 2026 16:56:28 -0500 Subject: [PATCH 3/3] changeset --- .changeset/every-colts-hammer.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/every-colts-hammer.md diff --git a/.changeset/every-colts-hammer.md b/.changeset/every-colts-hammer.md new file mode 100644 index 000000000..8f116f9ba --- /dev/null +++ b/.changeset/every-colts-hammer.md @@ -0,0 +1,5 @@ +--- +"@knocklabs/client": patch +--- + +[guides] support search param in guide activation url patterns