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

Send enrollment dates in ET, and align conversion windows with ET days. #2950

Merged
merged 3 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions shared/js/background/components/abn-experiments.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,11 @@ export default class AbnExperimentMetrics {
markExperimentEnrolled(featureName, subFeatureName, metrics) {
const cohort = this.remoteConfig.getCohort(featureName, subFeatureName);
if (cohort && !cohort.enrolledAt) {
cohort.enrolledAt = Date.now();
// We set the enrollment timestamp as the start of the current day in ET, so that all conversion windows align with ET date changes.
cohort.enrolledAt = startOfDayEST(Date.now());
cohort.metrics = (metrics || generateRetentionMetrics()).map((m) => ({ ...m, counter: 0, sent: false }));
sendPixelRequest(`experiment_enroll_${subFeatureName}_${cohort.name}`, {
enrollmentDate: new Date(cohort.enrolledAt).toISOString().slice(0, 10),
enrollmentDate: getDateStringEST(cohort.enrolledAt),
});
// updated stored cohort metadata
this.remoteConfig.setCohort(featureName, subFeatureName, cohort);
Expand Down Expand Up @@ -120,7 +121,7 @@ export default class AbnExperimentMetrics {
if (!cohort?.metrics || !cohort?.enrolledAt) {
return;
}
const enrollmentDate = new Date(cohort.enrolledAt).toISOString().slice(0, 10);
const enrollmentDate = getDateStringEST(cohort.enrolledAt);
const daysSinceEnrollment = Math.floor(((timestamp || Date.now()) - cohort.enrolledAt) / (1000 * 60 * 60 * 24));
// Find metrics for this experiment that match at this point in time.
// i.e. we are within the conversion window, and haven't sent the pixel yet.
Expand Down Expand Up @@ -155,3 +156,30 @@ export default class AbnExperimentMetrics {
});
}
}

/**
* Given a timestamp or date, returns a string in ISO (YYYY-MM-DD) format corresponding for the date
* at that instant in Eastern Summer Time (UTC-5).
* @param {number} timestamp
* @returns {string} Date string in ISO (YYYY-MM-DD) format.
*/
export function getDateStringEST(timestamp) {
// toLocaleDateString can return the date in our chosen timezone (in this case ET), however the output is also localized.
// We can use the 'sv' locale to get a date format that matches ISO (YYYY-MM-DD)
return new Date(timestamp).toLocaleDateString('sv', { timeZone: '-05:00' });
}

/**
* Calculates the unix timestamp for the start of the current day in Eastern Summer Time (UTC-5) from the provided
* timestamp.
* @param {number} now
* @returns {number} Timestamp for midnight on the current day in EST.
*/
export function startOfDayEST(now = Date.now()) {
const d = new Date(now);
// Before 05:00 UTC, move back one day
if (d.getUTCHours() < 5) {
d.setUTCDate(d.getUTCDate() - 1);
}
return d.setUTCHours(5, 0, 0, 0);
}
2 changes: 1 addition & 1 deletion shared/js/background/metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class AppUseMetric {
// trigger on construction: happens whenever the service worker is spun up, which should correlate with browser activity.
// Note: we don't care about overtriggering here, as the experiment framework will handle deduplication.
// This metric will only correlate with when the browser is open, and not necessarily if it is being 'used'.
abnMetrics.remoteConfig.ready.then(() => abnMetrics.onMetricTriggered('app_use'));
abnMetrics.remoteConfig.allLoadingFinished.then(() => setTimeout(() => abnMetrics.onMetricTriggered('app_use'), 5000));
}
}

Expand Down
27 changes: 26 additions & 1 deletion unit-test/background/abn-framework.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ParamsValidator } from '@duckduckgo/pixel-schema/src/params_validator.m

import messageHandlers from '../../shared/js/background/message-handlers';
import RemoteConfig, { choseCohort } from '../../shared/js/background/components/remote-config';
import AbnExperimentMetrics from '../../shared/js/background/components/abn-experiments';
import AbnExperimentMetrics, { getDateStringEST, startOfDayEST } from '../../shared/js/background/components/abn-experiments';
import load from '../../shared/js/background/load';
import commonParams from '../../pixel-definitions/common_params.json';
import commonSuffixes from '../../pixel-definitions/common_suffixes.json';
Expand Down Expand Up @@ -351,3 +351,28 @@ describe('ABN pixels', () => {
expect(pixelRequests.pop()).toContain('conversionWindowDays=5-7&value=4');
});
});

describe('startOfDayEST', () => {
it('always returns a timestamp in the past', () => {
expect(startOfDayEST()).toBeLessThan(Date.now());
});

it('returns a timestamp corresponding to the start of the current day in ET', () => {
const ts = new Date('2025-02-01T06:00:00');
expect(new Date(startOfDayEST(ts))).toEqual(new Date('Feb 1 2025 00:00:00 UTC-0500'));
ts.setUTCHours(23);
expect(new Date(startOfDayEST(ts))).toEqual(new Date('Feb 1 2025 00:00:00 UTC-0500'));
});

it('handles day rollover', () => {
const ts = new Date('2025-02-01T03:00:00');
expect(new Date(startOfDayEST(ts))).toEqual(new Date('Jan 31 2025 00:00:00 UTC-0500'));
});
});

describe('getDateStringEST', () => {
it('returns a unix timestamp as the date in ET at that instant', () => {
expect(getDateStringEST(new Date('2025-02-01T06:00:00'))).toEqual('2025-02-01');
expect(getDateStringEST(new Date('2025-02-01T02:00:00'))).toEqual('2025-01-31');
});
});
Loading