228 lines
6.4 KiB
JavaScript
228 lines
6.4 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 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;
|
|
}
|
|
|
|
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)
|
|
);
|
|
}
|
|
}
|