diff --git a/src/server/api/auxiaProxyRouter.ts b/src/server/api/auxiaProxyRouter.ts index 5b1ebf014..de3cf718e 100644 --- a/src/server/api/auxiaProxyRouter.ts +++ b/src/server/api/auxiaProxyRouter.ts @@ -68,11 +68,13 @@ export const buildAuxiaProxyRouter = (config: AuxiaRouterConfig): Router => { // Nullable attributes: // 'browserId' // 'showDefaultGate' + // 'hideSupportMessagingTimestamp' async (req: express.Request, res: express.Response, next: express.NextFunction) => { try { + const now = Date.now(); // current time in milliseconds since epoch const payload = req.body as GetTreatmentsRequestPayload; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); const envelop = await gateTypeToUserTreatmentsEnvelop(config, gateType, payload); if (envelop !== undefined) { const data = userTreatmentsEnvelopToProxyGetTreatmentsAnswerData(envelop); diff --git a/src/server/signin-gate/libPure.ts b/src/server/signin-gate/libPure.ts index 795bc8f9c..d317c8bdc 100644 --- a/src/server/signin-gate/libPure.ts +++ b/src/server/signin-gate/libPure.ts @@ -362,9 +362,46 @@ export const userHasConsented = (payload: GetTreatmentsRequestPayload): boolean return payload.hasConsented; }; +export const hideSupportMessagingHasOverride = ( + payload: GetTreatmentsRequestPayload, + now: number, +): boolean => { + // Purpose: + // Return true if we have a hideSupportMessagingTimestamp and it's less than 30 days old + + // Parameters: + // - payload + // - now: current time in milliseconds since epoch + // (nb: We pass now instead of getting it within the body + // to make the function pure and testable) + + // Date: 1 September 2025 + // + // The payload.hideSupportMessagingTimestamp, could be in the future, + // this happens if the user has performed a recurring contribution. + // We have guarded against that situation client side + // https://github.com/guardian/dotcom-rendering/pull/14462 + // but also guard against it here. + + if (payload.hideSupportMessagingTimestamp === undefined) { + return false; + } + if (!Number.isInteger(payload.hideSupportMessagingTimestamp)) { + return false; + } + if (payload.hideSupportMessagingTimestamp > now) { + return false; + } + const limit = 86400 * 30 * 1000; // milliseconds over 30 days + return now - payload.hideSupportMessagingTimestamp < limit; +}; + export const getTreatmentsRequestPayloadToGateType = ( payload: GetTreatmentsRequestPayload, + now: number, ): GateType => { + // now: current time in milliseconds since epoch + // This function is a pure function (without any side effects) which gets the body // of a '/auxia/get-treatments' request and returns the correct GateType // It was introduced to separate the choice of the gate from it's actual build, @@ -450,8 +487,13 @@ export const getTreatmentsRequestPayloadToGateType = ( // // effects: // - Notify Auxia for analytics - // - No gate display the first 3 page views - // - Gate: 3x dismissal, then mandatory + // - Guardian drives the gate: + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - 3x dismissal, then mandatory + if (hideSupportMessagingHasOverride(payload, now)) { + return 'AuxiaAnalyticsThenNone'; + } if (payload.dailyArticleCount < 3) { return 'AuxiaAnalyticsThenNone'; } @@ -477,9 +519,12 @@ export const getTreatmentsRequestPayloadToGateType = ( // effects: // - No Auxia notification // - Guardian drives the gate: - // - No gate display the first 3 page views - // - Gate: dismissible gates - // then no gate after 5 dismisses + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses + if (hideSupportMessagingHasOverride(payload, now)) { + return 'None'; + } if (payload.dailyArticleCount < 3) { return 'None'; } @@ -505,8 +550,12 @@ export const getTreatmentsRequestPayloadToGateType = ( // effects: // - Notify Auxia for analytics // - Guardian drives the gate: - // - No gate display the first 3 page views - // - Gate: 3x dismissal, then mandatory + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - 3x dismissal, then mandatory + if (hideSupportMessagingHasOverride(payload, now)) { + return 'AuxiaAnalyticsThenNone'; + } if (payload.dailyArticleCount < 3) { return 'AuxiaAnalyticsThenNone'; } @@ -551,9 +600,12 @@ export const getTreatmentsRequestPayloadToGateType = ( // effects: // - No Auxia notification // - Guardian drives the gate: + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) // - No gate display the first 3 page views - // - Gate: dismissible gates - // then no gate after 5 dismisses + // - Dismissible gates then no gate after 5 dismisses + if (hideSupportMessagingHasOverride(payload, now)) { + return 'None'; + } if (payload.dailyArticleCount < 3) { return 'None'; } @@ -579,9 +631,12 @@ export const getTreatmentsRequestPayloadToGateType = ( // effects: // - No Auxia notification // - Guardian drives the gate: - // - No gate display the first 3 page views - // - Gate: dismissible gates - // then no gate after 5 dismisses + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses + if (hideSupportMessagingHasOverride(payload, now)) { + return 'None'; + } if (payload.dailyArticleCount < 3) { return 'None'; } @@ -607,9 +662,12 @@ export const getTreatmentsRequestPayloadToGateType = ( // effects: // - No Auxia notification // - Guardian drives the gate: - // - No gate display the first 3 page views - // - Gate: dismissible gates - // then no gate after 5 dismisses + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses + if (hideSupportMessagingHasOverride(payload, now)) { + return 'None'; + } if (payload.dailyArticleCount < 3) { return 'None'; } diff --git a/src/server/signin-gate/libPureTests/getTreatmentsRequestPayloadToGateType/ireland.test.ts b/src/server/signin-gate/libPureTests/getTreatmentsRequestPayloadToGateType/ireland.test.ts index fbc736f33..2b625e614 100644 --- a/src/server/signin-gate/libPureTests/getTreatmentsRequestPayloadToGateType/ireland.test.ts +++ b/src/server/signin-gate/libPureTests/getTreatmentsRequestPayloadToGateType/ireland.test.ts @@ -30,8 +30,10 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('AuxiaAPI'); }); @@ -46,9 +48,9 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { // effects: // - No Auxia notification // - Guardian drives the gate: - // - No gate display the first 3 page views - // - Gate: dismissible gates - // then no gate after 5 dismisses + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses const payload: GetTreatmentsRequestPayload = { browserId: 'sample', @@ -67,8 +69,10 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 4, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('None'); }); @@ -83,9 +87,9 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { // effects: // - No Auxia notification // - Guardian drives the gate: - // - No gate display the first 3 page views - // - Gate: dismissible gates - // then no gate after 5 dismisses + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses const payload: GetTreatmentsRequestPayload = { browserId: 'sample', @@ -104,8 +108,10 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 4, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('GuDismissible'); }); @@ -120,9 +126,9 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { // effects: // - No Auxia notification // - Guardian drives the gate: - // - No gate display the first 3 page views - // - Gate: dismissible gates - // then no gate after 5 dismisses + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses const payload: GetTreatmentsRequestPayload = { browserId: 'sample', @@ -141,8 +147,10 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 8, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('None'); }); @@ -156,8 +164,10 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { // // effects: // - Notify Auxia for analytics - // - No gate display the first 3 page views - // - Gate: 3x dismissal, then mandatory + // - Guardian drives the gate: + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - 3x dismissal, then mandatory const payload: GetTreatmentsRequestPayload = { browserId: 'sample', @@ -176,8 +186,10 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 8, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('AuxiaAnalyticsThenNone'); }); @@ -191,8 +203,10 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { // // effects: // - Notify Auxia for analytics - // - No gate display the first 3 page views - // - Gate: 3x dismissal, then mandatory + // - Guardian drives the gate: + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - 3x dismissal, then mandatory const payload: GetTreatmentsRequestPayload = { browserId: 'sample', @@ -211,8 +225,10 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 1, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('AuxiaAnalyticsThenGuDismissible'); }); @@ -226,8 +242,10 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { // // effects: // - Notify Auxia for analytics - // - No gate display the first 3 page views - // - Gate: 3x dismissal, then mandatory + // - Guardian drives the gate: + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - 3x dismissal, then mandatory const payload: GetTreatmentsRequestPayload = { browserId: 'sample', @@ -246,8 +264,10 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 5, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('AuxiaAnalyticsThenGuMandatory'); }); @@ -262,6 +282,7 @@ describe('getTreatmentsRequestPayloadToGateType (ireland)', () => { // effects: // - Notify Auxia for analytics // - Guardian drives the gate: - // - No gate display the first 3 page views - // - Gate: 3x dismissal, then mandatory + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - 3x dismissal, then mandatory }); diff --git a/src/server/signin-gate/libPureTests/getTreatmentsRequestPayloadToGateType/special-cases.test.ts b/src/server/signin-gate/libPureTests/getTreatmentsRequestPayloadToGateType/special-cases.test.ts index 5b81d56c5..94725c789 100644 --- a/src/server/signin-gate/libPureTests/getTreatmentsRequestPayloadToGateType/special-cases.test.ts +++ b/src/server/signin-gate/libPureTests/getTreatmentsRequestPayloadToGateType/special-cases.test.ts @@ -20,8 +20,10 @@ describe('getTreatmentsRequestPayloadToGateType (special cases)', () => { shouldServeDismissible: true, showDefaultGate: 'mandatory', gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('None'); }); @@ -43,8 +45,10 @@ describe('getTreatmentsRequestPayloadToGateType (special cases)', () => { shouldServeDismissible: true, showDefaultGate: 'mandatory', gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('None'); }); @@ -66,8 +70,10 @@ describe('getTreatmentsRequestPayloadToGateType (special cases)', () => { shouldServeDismissible: true, showDefaultGate: 'mandatory', gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('None'); }); @@ -89,8 +95,10 @@ describe('getTreatmentsRequestPayloadToGateType (special cases)', () => { shouldServeDismissible: true, showDefaultGate: 'mandatory', gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('None'); }); @@ -112,8 +120,10 @@ describe('getTreatmentsRequestPayloadToGateType (special cases)', () => { shouldServeDismissible: true, // <- [tested] showDefaultGate: 'mandatory', // <- [tested] gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('GuDismissible'); }); @@ -135,8 +145,10 @@ describe('getTreatmentsRequestPayloadToGateType (special cases)', () => { shouldServeDismissible: true, // <- [tested] showDefaultGate: 'dismissible', // <- [tested] gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('GuDismissible'); }); @@ -158,8 +170,10 @@ describe('getTreatmentsRequestPayloadToGateType (special cases)', () => { shouldServeDismissible: false, showDefaultGate: 'mandatory', // <- [tested] gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('GuMandatory'); }); @@ -181,8 +195,10 @@ describe('getTreatmentsRequestPayloadToGateType (special cases)', () => { shouldServeDismissible: false, showDefaultGate: 'dismissible', // <- [tested] gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('GuDismissible'); }); }); diff --git a/src/server/signin-gate/libPureTests/getTreatmentsRequestPayloadToGateType/world-without-ireland.test.ts b/src/server/signin-gate/libPureTests/getTreatmentsRequestPayloadToGateType/world-without-ireland.test.ts index 6d05fe38b..3e0917fed 100644 --- a/src/server/signin-gate/libPureTests/getTreatmentsRequestPayloadToGateType/world-without-ireland.test.ts +++ b/src/server/signin-gate/libPureTests/getTreatmentsRequestPayloadToGateType/world-without-ireland.test.ts @@ -13,9 +13,9 @@ describe('getTreatmentsRequestPayloadToGateType', () => { // effects: // - No Auxia notification // - Guardian drives the gate: - // - No gate display the first 3 page views - // - Gate: dismissible gates - // then no gate after 5 dismisses + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses const payload: GetTreatmentsRequestPayload = { browserId: 'sample', @@ -34,8 +34,10 @@ describe('getTreatmentsRequestPayloadToGateType', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('None'); }); it('logic.md [02], first dismissible gates', () => { @@ -49,9 +51,9 @@ describe('getTreatmentsRequestPayloadToGateType', () => { // effects: // - No Auxia notification // - Guardian drives the gate: - // - No gate display the first 3 page views - // - Gate: dismissible gates - // then no gate after 5 dismisses + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses const payload: GetTreatmentsRequestPayload = { browserId: 'sample', @@ -70,8 +72,10 @@ describe('getTreatmentsRequestPayloadToGateType', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 1, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('GuDismissible'); }); it('logic.md [02], high gate dismiss count', () => { @@ -85,9 +89,9 @@ describe('getTreatmentsRequestPayloadToGateType', () => { // effects: // - No Auxia notification // - Guardian drives the gate: - // - No gate display the first 3 page views - // - Gate: dismissible gates - // then no gate after 5 dismisses + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses const payload: GetTreatmentsRequestPayload = { browserId: 'sample', @@ -106,8 +110,10 @@ describe('getTreatmentsRequestPayloadToGateType', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 7, + hideSupportMessagingTimestamp: undefined, }; - const gateType = getTreatmentsRequestPayloadToGateType(payload); + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); expect(gateType).toStrictEqual('None'); }); }); diff --git a/src/server/signin-gate/libPureTests/hideSupportMessagingHasOverride.test.ts b/src/server/signin-gate/libPureTests/hideSupportMessagingHasOverride.test.ts new file mode 100644 index 000000000..684b4baa8 --- /dev/null +++ b/src/server/signin-gate/libPureTests/hideSupportMessagingHasOverride.test.ts @@ -0,0 +1,263 @@ +import { getTreatmentsRequestPayloadToGateType, hideSupportMessagingHasOverride } from '../libPure'; +import type { GetTreatmentsRequestPayload } from '../types'; + +it('hideSupportMessagingHasOverride, undefined', () => { + // [02] (copy from logic.md) + // + // prerequisites: + // - World without Ireland + // - Is Guardian share of the audience + // - user has consented + // + // effects: + // - No Auxia notification + // - Guardian drives the gate: + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses + + const payload = { + browserId: 'sample', + isSupporter: false, + dailyArticleCount: 0, + articleIdentifier: 'sample: article identifier', + editionId: 'GB', + contentType: 'Article', + sectionId: 'uk-news', + tagIds: ['type/article'], + gateDismissCount: 0, + countryCode: 'FR', // <- [outside ireland] + mvtId: 450_000, // <- [Guardian] + should_show_legacy_gate_tmp: true, + hasConsented: true, // <- [consented] + shouldServeDismissible: false, + showDefaultGate: undefined, + gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, // <- tested: no information provided + }; + + const now = 1756568890120; // 2025-08-30 16:48:10 +0100 + expect(hideSupportMessagingHasOverride(payload, now)).toBe(false); +}); + +it('hideSupportMessagingHasOverride, defined, more than 30 days ago', () => { + // [02] (copy from logic.md) + // + // prerequisites: + // - World without Ireland + // - Is Guardian share of the audience + // - user has consented + // + // effects: + // - No Auxia notification + // - Guardian drives the gate: + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses + + const payload = { + browserId: 'sample', + isSupporter: false, + dailyArticleCount: 0, + articleIdentifier: 'sample: article identifier', + editionId: 'GB', + contentType: 'Article', + sectionId: 'uk-news', + tagIds: ['type/article'], + gateDismissCount: 0, + countryCode: 'FR', // <- [outside ireland] + mvtId: 450_000, // <- [Guardian] + should_show_legacy_gate_tmp: true, + hasConsented: true, // <- [consented] + shouldServeDismissible: false, + showDefaultGate: undefined, + gateDisplayCount: 0, + hideSupportMessagingTimestamp: 1674172805000, // <- tested: 2023-01-20T00:00:05Z + }; + + const now = 1756568890120; // 2025-08-30 16:48:10 +0100 (more than 30 days) + expect(hideSupportMessagingHasOverride(payload, now)).toBe(false); +}); + +it('hideSupportMessagingHasOverride, defined, less than 30 days ago', () => { + // [02] (copy from logic.md) + // + // prerequisites: + // - World without Ireland + // - Is Guardian share of the audience + // - user has consented + // + // effects: + // - No Auxia notification + // - Guardian drives the gate: + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses + + const payload = { + browserId: 'sample', + isSupporter: false, + dailyArticleCount: 0, + articleIdentifier: 'sample: article identifier', + editionId: 'GB', + contentType: 'Article', + sectionId: 'uk-news', + tagIds: ['type/article'], + gateDismissCount: 0, + countryCode: 'FR', // <- [outside ireland] + mvtId: 450_000, // <- [Guardian] + should_show_legacy_gate_tmp: true, + hasConsented: true, // <- [consented] + shouldServeDismissible: false, + showDefaultGate: undefined, + gateDisplayCount: 0, + hideSupportMessagingTimestamp: 1755644400000, // <- tested: 2025-08-20 00:00:00 +0100 + }; + + const now = 1756568890120; // 2025-08-30 16:48:10 +0100 (less than 30 days) + expect(hideSupportMessagingHasOverride(payload, now)).toBe(true); +}); + +it('hideSupportMessagingHasOverride, defined, value in the future', () => { + const payload = { + browserId: 'sample', + isSupporter: false, + dailyArticleCount: 0, + articleIdentifier: 'sample: article identifier', + editionId: 'GB', + contentType: 'Article', + sectionId: 'uk-news', + tagIds: ['type/article'], + gateDismissCount: 0, + countryCode: 'FR', // <- [outside ireland] + mvtId: 450_000, // <- [Guardian] + should_show_legacy_gate_tmp: true, + hasConsented: true, // <- [consented] + shouldServeDismissible: false, + showDefaultGate: undefined, + gateDisplayCount: 0, + hideSupportMessagingTimestamp: 1756722953000, // <- tested: 2025-09-01 11:35:53 +0100 (future value relatively to now) + }; + + const now = 1756568890120; // 2025-08-30 16:48:10 +0100 (less than 30 days) + expect(hideSupportMessagingHasOverride(payload, now)).toBe(false); +}); + +// We also test getTreatmentsRequestPayloadToGateType + +it('getTreatmentsRequestPayloadToGateType, without override', () => { + // [02] (copy from logic.md) + // + // prerequisites: + // - World without Ireland + // - Is Guardian share of the audience + // - user has consented + // + // effects: + // - No Auxia notification + // - Guardian drives the gate: + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses + + const payload: GetTreatmentsRequestPayload = { + browserId: 'sample', + isSupporter: false, + dailyArticleCount: 5, + articleIdentifier: 'sample: article identifier', + editionId: 'GB', + contentType: 'Article', + sectionId: 'uk-news', + tagIds: ['type/article'], + gateDismissCount: 0, + countryCode: 'FR', // <- [outside ireland] + mvtId: 450_000, // <- [Guardian] + should_show_legacy_gate_tmp: true, + hasConsented: true, // <- [consented] + shouldServeDismissible: false, + showDefaultGate: undefined, + gateDisplayCount: 1, + hideSupportMessagingTimestamp: undefined, // <- no override + }; + const now = 1756568322187; // current time in milliseconds since epoch + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); + expect(gateType).toStrictEqual('GuDismissible'); +}); + +it('getTreatmentsRequestPayloadToGateType, with override', () => { + // [02] (copy from logic.md) + // + // prerequisites: + // - World without Ireland + // - Is Guardian share of the audience + // - user has consented + // + // effects: + // - No Auxia notification + // - Guardian drives the gate: + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - Dismissible gates then no gate after 5 dismisses + + const payload: GetTreatmentsRequestPayload = { + browserId: 'sample', + isSupporter: false, + dailyArticleCount: 5, + articleIdentifier: 'sample: article identifier', + editionId: 'GB', + contentType: 'Article', + sectionId: 'uk-news', + tagIds: ['type/article'], + gateDismissCount: 0, + countryCode: 'FR', // <- [outside ireland] + mvtId: 450_000, // <- [Guardian] + should_show_legacy_gate_tmp: true, + hasConsented: true, // <- [consented] + shouldServeDismissible: false, + showDefaultGate: undefined, + gateDisplayCount: 1, + hideSupportMessagingTimestamp: 1755644400000, // <- tested: 2025-08-20 00:00:00 +0100 + }; + const now = 1756568890120; // 2025-08-30 16:48:10 +0100 (less than 30 days) + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); + expect(gateType).toStrictEqual('None'); +}); + +it('getTreatmentsRequestPayloadToGateType, with override, ireland with Auxia Analytics', () => { + // [05] (copy from logic.md) + // + // prerequisites: + // - Ireland + // - Is Auxia share of the audience + // - user has NOT consented + // + // effects: + // - Notify Auxia for analytics + // - Guardian drives the gate: + // - No gate for 30 days after a single contribution event (gu_hide_support_messaging; hideSupportMessagingTimestamp) + // - No gate display the first 3 page views + // - 3x dismissal, then mandatory + + const payload: GetTreatmentsRequestPayload = { + browserId: 'sample', + isSupporter: false, + dailyArticleCount: 5, + articleIdentifier: 'sample: article identifier', + editionId: 'GB', + contentType: 'Article', + sectionId: 'uk-news', + tagIds: ['type/article'], + gateDismissCount: 1, + countryCode: 'IE', // <- [Ireland] + mvtId: 250_000, // <- [Auxia] + should_show_legacy_gate_tmp: true, + hasConsented: false, // <- [not consented] + shouldServeDismissible: false, + showDefaultGate: undefined, + gateDisplayCount: 1, + hideSupportMessagingTimestamp: 1755644400000, // <- tested: 2025-08-20 00:00:00 +0100 + }; + const now = 1756568890120; // 2025-08-30 16:48:10 +0100 (less than 30 days) + const gateType = getTreatmentsRequestPayloadToGateType(payload, now); + expect(gateType).toStrictEqual('AuxiaAnalyticsThenNone'); // Instead of AuxiaAnalyticsThenGuDismissible +}); diff --git a/src/server/signin-gate/libPureTests/isAuxiaAudienceShare.test.ts b/src/server/signin-gate/libPureTests/isAuxiaAudienceShare.test.ts index 4e03d19d0..19b6a0ab1 100644 --- a/src/server/signin-gate/libPureTests/isAuxiaAudienceShare.test.ts +++ b/src/server/signin-gate/libPureTests/isAuxiaAudienceShare.test.ts @@ -19,6 +19,7 @@ it('isAuxiaAudienceShare', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; const payload2: GetTreatmentsRequestPayload = { browserId: 'sample', @@ -37,6 +38,7 @@ it('isAuxiaAudienceShare', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; expect(isAuxiaAudienceShare(payload1)).toBe(true); diff --git a/src/server/signin-gate/libPureTests/isGuardianAudienceShare.test.ts b/src/server/signin-gate/libPureTests/isGuardianAudienceShare.test.ts index f5fa4162a..d0e093516 100644 --- a/src/server/signin-gate/libPureTests/isGuardianAudienceShare.test.ts +++ b/src/server/signin-gate/libPureTests/isGuardianAudienceShare.test.ts @@ -19,6 +19,7 @@ it('isGuardianAudienceShare', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; const payload2: GetTreatmentsRequestPayload = { browserId: 'sample', @@ -37,6 +38,7 @@ it('isGuardianAudienceShare', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; expect(isGuardianAudienceShare(payload1)).toBe(false); diff --git a/src/server/signin-gate/libPureTests/isStaffTestConditionShowDefaultGate.test.ts b/src/server/signin-gate/libPureTests/isStaffTestConditionShowDefaultGate.test.ts index a34230178..9b63d05d6 100644 --- a/src/server/signin-gate/libPureTests/isStaffTestConditionShowDefaultGate.test.ts +++ b/src/server/signin-gate/libPureTests/isStaffTestConditionShowDefaultGate.test.ts @@ -19,6 +19,7 @@ it('isStaffTestConditionShowDefaultGate', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; expect(isStaffTestConditionShowDefaultGate(payload1)).toBe(false); @@ -39,6 +40,7 @@ it('isStaffTestConditionShowDefaultGate', () => { shouldServeDismissible: true, showDefaultGate: 'true', gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; expect(isStaffTestConditionShowDefaultGate(payload2)).toBe(true); @@ -59,6 +61,7 @@ it('isStaffTestConditionShowDefaultGate', () => { shouldServeDismissible: true, showDefaultGate: 'dismissible', gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; expect(isStaffTestConditionShowDefaultGate(payload3)).toBe(true); }); diff --git a/src/server/signin-gate/libPureTests/staffTestConditionToDefaultGate.test.ts b/src/server/signin-gate/libPureTests/staffTestConditionToDefaultGate.test.ts index 4a5ef119e..90f0417c6 100644 --- a/src/server/signin-gate/libPureTests/staffTestConditionToDefaultGate.test.ts +++ b/src/server/signin-gate/libPureTests/staffTestConditionToDefaultGate.test.ts @@ -19,6 +19,7 @@ it('staffTestConditionToDefaultGate', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; expect(staffTestConditionToDefaultGate(payload1)).toBe('None'); @@ -39,6 +40,7 @@ it('staffTestConditionToDefaultGate', () => { shouldServeDismissible: true, showDefaultGate: 'true', gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; expect(staffTestConditionToDefaultGate(payload2)).toBe('GuDismissible'); @@ -59,6 +61,7 @@ it('staffTestConditionToDefaultGate', () => { shouldServeDismissible: true, showDefaultGate: 'dismissible', gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; expect(staffTestConditionToDefaultGate(payload3)).toBe('GuDismissible'); @@ -79,6 +82,7 @@ it('staffTestConditionToDefaultGate', () => { shouldServeDismissible: true, showDefaultGate: 'mandatory', gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; expect(staffTestConditionToDefaultGate(payload4)).toBe('GuMandatory'); }); diff --git a/src/server/signin-gate/libPureTests/userHasConsented.test.ts b/src/server/signin-gate/libPureTests/userHasConsented.test.ts index 12ae39ab8..f69ca8c26 100644 --- a/src/server/signin-gate/libPureTests/userHasConsented.test.ts +++ b/src/server/signin-gate/libPureTests/userHasConsented.test.ts @@ -19,6 +19,7 @@ it('userHasConsented', () => { shouldServeDismissible: false, showDefaultGate: undefined, gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; expect(userHasConsented(payload1)).toBe(true); @@ -39,6 +40,7 @@ it('userHasConsented', () => { shouldServeDismissible: true, showDefaultGate: 'true', gateDisplayCount: 0, + hideSupportMessagingTimestamp: undefined, }; expect(userHasConsented(payload2)).toBe(false); }); diff --git a/src/server/signin-gate/logic.md b/src/server/signin-gate/logic.md index 9f3d8efb1..4fb51f09f 100644 --- a/src/server/signin-gate/logic.md +++ b/src/server/signin-gate/logic.md @@ -24,22 +24,28 @@ nb: the numbers, for instance, [01], uniquely identify the experience for the co | | | | - No Auxia notification | - No Auxia notification | un-consented | - Guardian drives the gate: | - Guardian drives the gate: | - | - No gate display the first 3 page views | - No gate display the first 3 page views | - | - Gate: dismissible gates, | - Gate: dismissible gates | - | then no gate after 5 dismisses | then no gate after 5 dismisses | + | - No gate for 30 days after a single | - No gate for 30 days after a single | + | contribution event [01] | contribution event [01] | + | - No gate the first 3 page views | - No gate display the first 3 page views | + | - Dismissible gates, | - Dismissible gates | + | then no gate after 5 dismisses | then no gate after 5 dismisses | | | | -----------|----------------------------------------------------------------------------------------------------- | [03] | [04] | | | | | - Auxia drives the gate | - No Auxia notification | consented | | - Guardian drivess the gate: | + | | - No gate for 30 days after a single | + | | contribution event [01] | | | - No gate display the first 3 page views | - | | - Gate: dismissible gates | - | | then no gate after 5 dismisses | + | | - Dismissible gates | + | | then no gate after 5 dismisses | | | | --------------------------------------------------------------------------------------------- | | + +[01] use gu_hide_support_messaging cookie ``` ### Ireland @@ -53,19 +59,25 @@ nb: the numbers, for instance, [01], uniquely identify the experience for the co | | | | - Notify Auxia for analytics | - Notify Auxia for analytics | un-consented | - Guardian drives the gate: | - Guardian drives the gate: | - | - No gate display the first 3 page views | - No gate display the first 3 page views | - | - Gate: 3x dismissal, then mandatory | - Gate: 3x dismissal, then mandatory | + | - No gate for 30 days after a single | - No gate for 30 days after a single | + | contribution event [02] | contribution event [02] | + | - No gate the first 3 page views | - No gate the first 3 page views | + | - 3x dismissal, then mandatory | - 3x dismissal, then mandatory | | | | -----------|----------------------------------------------------------------------------------------------------- | [07] | [08] | | | | | - Auxia drives the gate | - No Auxia notification | consented | | - Guardian drives the gate: | - | | - No gate display the first 3 page views | - | | - Gate: dismissible gates | - | | then no gate after 5 dismisses | + | | - No gate for 30 days after a single | + | | contribution event [02] | + | | - No gate the first 3 page views | + | | - Dismissible gates | + | | then no gate after 5 dismisses | | | | --------------------------------------------------------------------------------------------- | | + +[02] use gu_hide_support_messaging cookie ``` diff --git a/src/server/signin-gate/types.ts b/src/server/signin-gate/types.ts index 86bfb9bf0..23de876ec 100644 --- a/src/server/signin-gate/types.ts +++ b/src/server/signin-gate/types.ts @@ -102,6 +102,7 @@ export interface GetTreatmentsRequestPayload { shouldServeDismissible: boolean; // [3] showDefaultGate: ShowGateValues; // [4] gateDisplayCount: number; // [5] + hideSupportMessagingTimestamp: number | undefined; // [6] } // [1] articleIdentifier examples: @@ -149,3 +150,13 @@ export interface GetTreatmentsRequestPayload { // For non consenting users outside ireland, the behavior doesn't change, we serve // dismissible gates + +// [6] + +// date: 30th August 2025 +// author: Pascal + +// `hideSupportMessagingTimestamp: number | undefined` was introduced to implement the effect +// of not showing the gate if the reader has performed a single contribution in the past 30 days. +// It is either undefined or return the timestamp carried by cookie `gu_hide_support_messaging` +// See: https://github.com/guardian/support-frontend/blob/7a5c0f9209054c24934b876771392531c261f51c/support-frontend/assets/helpers/storage/contributionsCookies.ts#L11