diff --git a/lib/config.ts b/lib/config.ts index d9a57c0..d7bd2f2 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -15,6 +15,8 @@ type ABTestConfig = { matcher_override?: MatcherOverride[]; // SkipMatchers specifies the list of matchers (node_id)s to skip. To skip 1p, simply send 1p skipMatchers?: string[]; + // SkipResolvers specifies the list of resolvers (node_id)s to skip. + skipResolvers?: string[]; }; type TargetingSignals = { diff --git a/lib/edge/targeting.test.js b/lib/edge/targeting.test.js index b59bb4a..2c82961 100644 --- a/lib/edge/targeting.test.js +++ b/lib/edge/targeting.test.js @@ -156,6 +156,29 @@ describe("determineABTest", () => { }); }); + test("handles tests with skipResolvers", () => { + const { determineABTest } = require("./targeting"); + const abTests = [ + { + id: "test1", + trafficPercentage: 50, + skipResolvers: ["resolver1"], + }, + { + id: "test2", + trafficPercentage: 50, + skipResolvers: ["resolver2"], + }, + ]; + + Math.random = jest.fn().mockReturnValue(0.25); // bucket = 25 + expect(determineABTest(abTests)).toEqual({ + id: "test1", + trafficPercentage: 50, + skipResolvers: ["resolver1"], + }); + }); + test("handles boundary conditions", () => { const { determineABTest } = require("./targeting"); const abTests = [ @@ -451,4 +474,103 @@ describe("Targeting function handles optional params like targeting signals and }) ); }); + + test("includes skip_resolvers parameter when abTest.skipResolvers is provided", async () => { + mockFetch.mockResolvedValue(createMockResponse({ audience: [], user: [] })); + + await Targeting( + { + ...baseConfig, + abTests: [{ id: "test1", trafficPercentage: 100, skipResolvers: ["resolver1"] }], + }, + { ...baseReq } + ); + + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining("skip_resolvers=resolver1"), + }) + ); + }); + + test("includes skip_resolvers parameter when abTest.skipResolvers contains multiple resolvers", async () => { + mockFetch.mockResolvedValue(createMockResponse({ audience: [], user: [] })); + + await Targeting( + { + ...baseConfig, + abTests: [{ id: "test1", trafficPercentage: 100, skipResolvers: ["resolver1", "resolver2"] }], + }, + { ...baseReq } + ); + + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining("skip_resolvers="), + }) + ); + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining(encodeURIComponent("resolver1,resolver2")), + }) + ); + }); + + test("does not include skip_resolvers parameter when abTest.skipResolvers is not provided", async () => { + mockFetch.mockResolvedValue(createMockResponse({ audience: [], user: [] })); + + await Targeting( + { + ...baseConfig, + abTests: [{ id: "test1", trafficPercentage: 100 }], + }, + { ...baseReq } + ); + + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.not.stringContaining("skip_resolvers="), + }) + ); + }); + + test("includes both skip_matchers and skip_resolvers when both are provided", async () => { + mockFetch.mockResolvedValue(createMockResponse({ audience: [], user: [] })); + + await Targeting( + { + ...baseConfig, + abTests: [ + { + id: "test1", + trafficPercentage: 100, + skipMatchers: ["matcher1", "matcher2"], + skipResolvers: ["resolver1", "resolver2"], + }, + ], + }, + { ...baseReq } + ); + + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining("skip_matchers="), + }) + ); + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining("skip_resolvers="), + }) + ); + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining(encodeURIComponent("matcher1,matcher2")), + }) + ); + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining(encodeURIComponent("resolver1,resolver2")), + }) + ); + }); }); diff --git a/lib/edge/targeting.ts b/lib/edge/targeting.ts index b13d9c8..a3996b9 100644 --- a/lib/edge/targeting.ts +++ b/lib/edge/targeting.ts @@ -80,6 +80,9 @@ async function Targeting(config: ResolvedConfig, req: TargetingRequest): Promise if (abTest.skipMatchers) { searchParams.append("skip_matchers", abTest.skipMatchers.join(",")); } + if (abTest.skipResolvers) { + searchParams.append("skip_resolvers", abTest.skipResolvers.join(",")); + } } if (config.additionalTargetingSignals?.ref) {