Files
tubestation/toolkit/components/messaging-system/targeting/Targeting.sys.mjs
Sandor Molnar 92076e16a7 Backed out 5 changesets (bug 1920562) for causing xpc assertion failures. CLOSED TREE
Backed out changeset 8f085ab589a8 (bug 1920562)
Backed out changeset 4405387ae770 (bug 1920562)
Backed out changeset a68fd13a33ae (bug 1920562)
Backed out changeset cd3672fc08ed (bug 1920562)
Backed out changeset 62ab18879eea (bug 1920562)
2024-10-08 00:16:13 +03:00

233 lines
6.5 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ASRouterTargeting:
// eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
"resource:///modules/asrouter/ASRouterTargeting.sys.mjs",
ClientEnvironment: "resource://normandy/lib/ClientEnvironment.sys.mjs",
ClientEnvironmentBase:
"resource://gre/modules/components-utils/ClientEnvironment.sys.mjs",
FilterExpressions:
"resource://gre/modules/components-utils/FilterExpressions.sys.mjs",
TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
const TARGETING_EVENT_CATEGORY = "messaging_experiments";
const DEFAULT_TIMEOUT = 5000;
const ERROR_TYPES = {
ATTRIBUTE_ERROR: "AttributeError",
TIMEOUT: "AttributeTimeout",
};
const TargetingEnvironment = {
get locale() {
return lazy.ASRouterTargeting.Environment.locale;
},
get localeLanguageCode() {
return lazy.ASRouterTargeting.Environment.localeLanguageCode;
},
get region() {
return lazy.ASRouterTargeting.Environment.region;
},
get userId() {
return lazy.ClientEnvironment.userId;
},
get version() {
return AppConstants.MOZ_APP_VERSION_DISPLAY;
},
get channel() {
const { settings } = lazy.TelemetryEnvironment.currentEnvironment;
return settings.update.channel;
},
get platform() {
return AppConstants.platform;
},
get os() {
return lazy.ClientEnvironmentBase.os;
},
};
export class TargetingContext {
#telemetrySource = null;
constructor(customContext, options = { source: null }) {
if (customContext) {
this.ctx = new Proxy(customContext, {
get: (customCtx, prop) => {
if (prop in TargetingEnvironment) {
return TargetingEnvironment[prop];
}
return customCtx[prop];
},
});
} else {
this.ctx = TargetingEnvironment;
}
// Used in telemetry to report where the targeting expression is coming from
this.#telemetrySource = options.source;
// Enable event recording
Services.telemetry.setEventRecordingEnabled(TARGETING_EVENT_CATEGORY, true);
}
setTelemetrySource(source) {
if (source) {
this.#telemetrySource = source;
}
}
_sendUndesiredEvent({ event, value }) {
let extra = { value };
if (this.#telemetrySource) {
extra.source = this.#telemetrySource;
}
Glean.messagingExperiments["targeting" + event].record(extra);
}
/**
* Wrap each property of context[key] with a Proxy that captures errors and
* timeouts
*
* @param {Object.<string, TargetingGetters> | TargetingGetters} context
* @param {string} key Namespace value found in `context` param
* @returns {TargetingGetters} Wrapped context where getter report errors and timeouts
*/
createContextWithTimeout(context, key = null) {
const timeoutDuration = key ? context[key].timeout : context.timeout;
const logUndesiredEvent = (event, key, prop) => {
const value = key ? `${key}.${prop}` : prop;
this._sendUndesiredEvent({ event, value });
console.error(`${event}: ${value}`);
};
return new Proxy(context, {
get(target, prop) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
// Create timeout cb to record attribute resolution taking too long.
let timeout = lazy.setTimeout(() => {
logUndesiredEvent(ERROR_TYPES.TIMEOUT, key, prop);
reject(
new Error(
`${prop} targeting getter timed out after ${
timeoutDuration || DEFAULT_TIMEOUT
}ms`
)
);
}, timeoutDuration || DEFAULT_TIMEOUT);
try {
resolve(await (key ? target[key][prop] : target[prop]));
} catch (error) {
logUndesiredEvent(ERROR_TYPES.ATTRIBUTE_ERROR, key, prop);
reject(error);
console.error(error);
} finally {
lazy.clearTimeout(timeout);
}
});
},
});
}
/**
* Merge all evaluation contexts and wrap the getters with timeouts
*
* @param {Object.<string, TargetingGetters>[]} contexts
* @returns {Object.<string, TargetingGetters>} Object that follows the pattern of `namespace: getters`
*/
mergeEvaluationContexts(contexts) {
let context = {};
for (let c of contexts) {
for (let envNamespace of Object.keys(c)) {
// Take the provided context apart, replace it with a proxy
context[envNamespace] = this.createContextWithTimeout(c, envNamespace);
}
}
return context;
}
/**
* Merge multiple TargetingGetters objects without accidentally evaluating
*
* @param {TargetingGetters[]} ...contexts
* @returns {Proxy<TargetingGetters>}
*/
static combineContexts(...contexts) {
return new Proxy(
{},
{
get(target, prop) {
for (let context of contexts) {
if (prop in context) {
return context[prop];
}
}
return null;
},
}
);
}
/**
* Evaluate JEXL expressions with default `TargetingEnvironment` and custom
* provided targeting contexts
*
* @example
* eval(
* "ctx.locale == 'en-US' && customCtx.foo == 42",
* { customCtx: { foo: 42 } }
* ); // true
*
* @param {string} expression JEXL expression
* @param {Object.<string, TargetingGetters>[]} ...contexts Additional custom context
* objects where the keys act as namespaces for the different getters
*
* @returns {promise} Evaluation result
*/
eval(expression, ...contexts) {
return lazy.FilterExpressions.eval(
expression,
this.mergeEvaluationContexts([{ ctx: this.ctx }, ...contexts])
);
}
/**
* Evaluate JEXL expressions with default provided targeting context
*
* @example
* new TargetingContext({ bar: 42 });
* evalWithDefault(
* "bar == 42",
* ); // true
*
* @param {string} expression JEXL expression
* @returns {promise} Evaluation result
*/
evalWithDefault(expression) {
return lazy.FilterExpressions.eval(
expression,
this.createContextWithTimeout(this.ctx)
);
}
}