Files
2025-11-04 05:03:50 +08:00

729 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { AbstractEventRecorder, Delegates } from '@amp-metrics/mt-metricskit-delegates-core';
import { EventRecorder as EventRecorder$1, ImmediateEventRecorder, environment, network, logger } from '@amp-metrics/mt-event-queue';
import { reflect, backoff } from '@amp-metrics/mt-metricskit-utils-private';
/*
* src/environment.js
* mt-metricskit-delegates-web
*
* Copyright © 2022 Apple Inc. All rights reserved.
*
*/
/**
* Provides a pre-built HTML delegate to use against the metrics.system.environment delegate via metrics.system.environment.setDelegate()
* If you want to use *most* of these methods, but not *all* of them, you can set this delegate and then create your own with whichever few methods you need to
* customize additionally, and then setDelegate() *that* delegate, in order to override those methods.
* @constructor
* @param {Config} config (optional) - An instance of Config, which contains the topic and the relevant configurations for the topic.
*/
function Environment(config) {
this._config = config;
}
/**
************************************ PSEUDO-PRIVATE METHODS/IVARS ************************************
* These functions need to be accessible for ease of testing, but should not be used by clients
*/
Environment.prototype._document = function _document() {
if (typeof document != 'undefined') {
return document;
} else {
throw "metricskit-delegates-html.environment HTML delegate 'document' object not found";
}
};
Environment.prototype._window = function _window() {
if (typeof window != 'undefined') {
return window;
} else {
throw "metricskit-delegates-html.environment HTML delegate 'window' object not found";
}
};
/**
************************************ PUBLIC METHODS/IVARS ************************************
*/
/**
* The name of the browser that generated the event, it only return browser field if "browserMaps" config property exists in the topic config
* Common userAgent format: "Mozilla/5.0 (<system-information>) <platform> (<platform-details>) <extensions>"
* This implementation will read the browser identifer from the first item in the extensions part of the userAgent.
* For those browsers which do not store their browser identifier in the first item (can be configured in "specifiedBrowsers" config, see the below example),
* the function will search the defined string in the userAgent to verify if it is the case.
* It will load the browser related configurations from "browserMaps" in the topic. The config looks like
* {
* specifiedBrowsers: string[], // listing the browsers identifiers which does not located in the first item in "extensions" in userAgent.
* browserMap: Record<string, string> // map the browser identifiers to readable string. For example, Edg -> Edge, OPR -> Opera
* }
* @example Safari
* @returns {Promise<string | null> | null}
* @overridable
*/
Environment.prototype.browser = function browser() {
var userAgent = this._window().navigator.userAgent;
if (!reflect.isDefinedNonNull(this._config) || !reflect.isDefinedNonNullNonEmpty(userAgent)) {
return null;
}
return this._config.value('browserMaps').then(function (browserConfig) {
if (!reflect.isDefinedNonNull(browserConfig)) {
return null;
}
var specifiedBrowsers = browserConfig.specifiedBrowsers || [];
var browserMap = browserConfig.browserMap || {};
var browserIdentifier = null;
for (var i = 0; i < specifiedBrowsers.length; i++) {
var specifiedBrowser = specifiedBrowsers[i];
if (userAgent.indexOf(specifiedBrowser) > -1) {
browserIdentifier = specifiedBrowser;
break;
}
}
if (!reflect.isDefinedNonNull(browserIdentifier)) {
// The Named capturing group: (?<name>...) requires "ECMAScript 2018"
var matches = userAgent.match(/Mozilla\/5.0 \((.*?)\)(\s|$)((.*?)\/(.*?)(\s)\(.*\))?(.*?)\/(.*?)(\s|$)/);
if (reflect.isDefinedNonNullNonEmpty(matches) && matches.length >= 8) {
browserIdentifier = matches[7];
}
}
if (reflect.isDefinedNonNull(browserIdentifier)) {
browserIdentifier = browserIdentifier.trim();
} else {
browserIdentifier = 'unknown';
}
var browser = browserMap[browserIdentifier] || browserIdentifier;
return browser;
});
};
/**
* The cookie string, e.g. "iTunes.cookie" (iTunes desktop), "iTunes.cookieForDefaultURL" (HTML iOS), "itms.cookie" (itml app), "document.cookie" (browser)
* NOTE: Callers should override this method if they want to supply a different cookie.
* @overridable
*/
Environment.prototype.cookie = function cookie() {
return this._window().document.cookie;
};
/**
* The URL that represents this page.
* Typically this is a "deep link" type URL.
* If no URL is available, this field may be omitted.
* @example "https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewGrouping?cc=us&mt=8"
* @returns {String}
* @overridable
*/
Environment.prototype.pageUrl = function pageUrl() {
return this._window().location.href;
};
/**
* The URL of the parent page, if the app is embedded in a parent context.
* Typically this is a "deep link" type URL.
* If no URL is available, or if the app is not embedded, this field may be omitted.
* @example "https://www.apple.com/blog/top-tracks.html"
* @returns {String}
* @overridable
* Note: due to iframe sandbox rules, the parent window's location may not be accessible.
* In that case, we fall back to document.referrer, which should be reliable if the app
* within the iframe is a single page app (document.referrer changes on every page turn).
* If the app in the iframe is not a single page app, we will have to persist the
* original referrer from the first page across page turns via e.g. localStorage.
* However, this use case is not currently needed by any client.
*/
Environment.prototype.parentPageUrl = function parentPageUrl() {
var windowObject = this._window();
var parentWindow = windowObject.parent;
var parentPageUrl;
if (parentWindow !== windowObject) {
try {
parentPageUrl = parentWindow.location.href;
} catch (e) {
parentPageUrl = this._document().referrer;
}
}
return parentPageUrl;
};
/**
* Pixel multiplier factor
* @example 2
* @returns {number}
* @overridable
*/
Environment.prototype.pixelRatio = function pixelRatio() {
return this._window().devicePixelRatio;
};
/**
* Client screen height in pixels
* @example 568
* @returns {number}
* @overridable
*/
Environment.prototype.screenHeight = function screenHeight() {
return this._window().screen.height;
};
/**
* Client screen width in pixels
* @example 320
* @returns {number}
* @overridable
*/
Environment.prototype.screenWidth = function screenWidth() {
return this._window().screen.width;
};
/**
* Clients user agent string. If the "app field is not provided, "userAgent may be used to derive the value of the "app field
* @example AppStore/2.0 iOS/8.3 model/iPhone7,2 build/12F70 (6; dt:106)
* @returns {String}
* @overridable
*/
Environment.prototype.userAgent = function userAgent() {
return this._window().navigator.userAgent;
};
/**
* App viewport height in pixels. Does not include window “chrome”, status bars, etc.
* Typically only available on desktop windowing systems.
* @example 1920
* @returns {number/undefined}
* @overridable
*/
Environment.prototype.windowInnerHeight = function windowInnerHeight() {
return this._window().innerHeight;
};
/**
* App viewport width in pixels. Does not include window “chrome”, status bars, etc.
* Typically only available on desktop windowing systems.
* @example 1080
* @returns {number/undefined}
* @overridable
*/
Environment.prototype.windowInnerWidth = function windowInnerWidth() {
return this._window().innerWidth;
};
/**
* Height in pixels of containing window, encompassing app viewport as well as window chrome, status bars, etc.
* Typically only available on desktop windowing systems.
* @example 1080
* @returns {number/undefined}
* @overridable
*/
Environment.prototype.windowOuterHeight = function windowOuterHeight() {
return this._window().outerHeight;
};
/**
* Width in pixels of containing window, encompassing app viewport as well as window chrome, status bars, etc.
* Typically only available on desktop windowing systems.
* @example 1920
* @returns {number/undefined}
* @overridable
*/
Environment.prototype.windowOuterWidth = function windowOuterWidth() {
return this._window().outerWidth;
};
/**
* The offset between W3C timing entry timestamps (which are relative to the page lifecycle) and the epoch time
* and the epoch time, in milliseconds
* @return {Number}
* @overridable
* Note: this is only currently used by PerfKit
* TODO: <rdar://problem/44976037> Refactor: Delegates: revisit HTML delegate packaging
*/
Environment.prototype.timeOriginOffset = function timeOriginOffset() {
var returnValue = null;
var performance = this._window().performance;
if (performance && performance.timing) {
returnValue = performance.timing.navigationStart;
}
return returnValue;
};
/**
* THE FOLLOWING DATA ARE UNAVAILABLE IN A PURE WEB BROWSER CONTEXT,
* BUT MAY BE IMPLEMENTED (VIA POTENTIALLY DIFFERENT APIS) IN VARIOUS HTML WEB VIEW CONTEXTS (iOS vs Desktop vs tvOS)
* THEY ARE LEFT UNIMPLEMENTED FOR CONTEXT-SPECIFIC DELEGATES TO OVERWRITE IF APPLICABLE
*/
/**
* The app identifier of the binary app
* NO DEFAULT IMPLEMENTATION... THIS METHOD SHOULD BE REPLACED
* @example "com.apple.appstore" or "com.apple.gamecenter"
* @returns {String}
* @overridable
*/
Environment.prototype.app = function app() {};
/**
* The version number of this application
* NO DEFAULT IMPLEMENTATION... THIS METHOD SHOULD BE REPLACED
* @example "1.0", "5.43", etc.
* @returns {String}
* @overridable
* @defaultimpl navigator.appVersion
*/
Environment.prototype.appVersion = function appVersion() {};
/**
* The total data capacity of the system, without regard for what's already been used or not.
* NO DEFAULT IMPLEMENTATION... THIS METHOD SHOULD BE REPLACED
* @returns {number}
* @overridable
*/
Environment.prototype.capacityData = function capacityData() {};
/**
* The total available data capacity of the system.
* NO DEFAULT IMPLEMENTATION... THIS METHOD SHOULD BE REPLACED
* @returns {number}
* @overridable
*/
Environment.prototype.capacityDataAvailable = function capacityDataAvailable() {};
/**
* The total disk capacity of the system, without regard for what's already been used or not.
* NO DEFAULT IMPLEMENTATION... THIS METHOD SHOULD BE REPLACED
* @returns {number}
* @overridable
*/
Environment.prototype.capacityDisk = function capacityDisk() {};
/**
* The total system capacity, without regard for what's already been used or not.
* NO DEFAULT IMPLEMENTATION... THIS METHOD SHOULD BE REPLACED
* @returns {number}
* @overridable
*/
Environment.prototype.capacitySystem = function capacitySystem() {};
/**
* The total available system capacity of the system.
* NO DEFAULT IMPLEMENTATION... THIS METHOD SHOULD BE REPLACED
* @api public
* @overridable
*/
Environment.prototype.capacitySystemAvailable = function capacitySystemAvailable() {};
/**
* Type of internet connection.
* Only applicable to devices
* Beware that users on WiFi may actually be receiving 3G speeds (i.e. if device is tethered to a portable hotspot.)
* NO DEFAULT IMPLEMENTATION... THIS METHOD SHOULD BE REPLACED
* @example "WiFi, "3G, etc.
* @returns {String}
* @overridable
*/
Environment.prototype.connectionType = function connectionType() {};
/**
* The id of this user ("directory service id").
* This id will get anonymized on the server prior to being saved.
* NO DEFAULT IMPLEMENTATION... THIS METHOD SHOULD BE REPLACED
* @example 659261189
* @returns {String}
* @overridable
*/
Environment.prototype.dsId = function dsId() {};
/**
* The hardware brand of the device. Not required for Apple devices.
* NO DEFAULT IMPLEMENTATION... THIS METHOD MUST BE REPLACED
* @example "Samsung", "LG", "Google"
* @returns {String}
*/
Environment.prototype.hardwareBrand = function hardwareBrand() {};
/**
* The hardware family of the device
* NO DEFAULT IMPLEMENTATION... THIS METHOD MUST BE REPLACED
* @example "iPhone", "Macbook Pro"
* @returns {String}
*/
Environment.prototype.hardwareFamily = function hardwareFamily() {};
/**
* The model of the device
* NO DEFAULT IMPLEMENTATION... THIS METHOD MUST BE REPLACED
* @example "iPhone10,2", "MacbookPro11,5"
* @returns {String}
*/
Environment.prototype.hardwareModel = function hardwareModel() {};
/**
* App that is hosting the storesheet or app
* NO DEFAULT IMPLEMENTATION... THIS METHOD SHOULD BE REPLACED
* @example com.rovio.AngryBirds
* @returns {String}
* @overridable
*/
Environment.prototype.hostApp = function hostApp() {};
/**
* Version of the app that is hosting the storesheet or app
* NO DEFAULT IMPLEMENTATION... HOWEVER: this field is optional
* @example "1.0.1"
* @returns {String}
* @overridable
*/
Environment.prototype.hostAppVersion = function hostAppVersion() {
// Optional field value
};
/**
* The name of the OS
* NO DEFAULT IMPLEMENTATION... THIS METHOD MUST BE REPLACED
* @example "ios", "macos", "windows"
* @returns {String}
*/
Environment.prototype.os = function os() {};
/**
* The build number of the OS
* NO DEFAULT IMPLEMENTATION... THIS METHOD MUST BE REPLACED
* @example "15D60", "17E192"
* @returns {String}
*/
Environment.prototype.osBuildNumber = function osBuildNumber() {};
/**
* A string array of language IDs, ordered in descending preference
* NO DEFAULT IMPLEMENTATION... THIS METHOD MUST BE REPLACED
* @example ["en-US", "fr-CA"]
* @returns {Array}
*/
Environment.prototype.osLanguages = function osLanguages() {};
/**
* The full OS version number
* In ITML, the value can be retrieved via Device.systemVersion
* NO DEFAULT IMPLEMENTATION... THIS METHOD SHOULD BE REPLACED
* @example "8.2.1" (iOS) "10.10.3" (Desktop)
* @returns {String}
* @overridable
*/
Environment.prototype.osVersion = function osVersion() {};
/**
* HTML resources revision number
* NO DEFAULT IMPLEMENTATION... THIS METHOD SHOULD BE REPLACED
* @example 2C97 or 8.4.0.0.103
* @returns {String}
* @overridable
*/
Environment.prototype.resourceRevNum = function resourceRevNum() {};
/**
* ISO 3166 Country Code. Apps that cannot provide a storeFrontHeader should provide a storeFrontCountryCode instead
* NO DEFAULT IMPLEMENTATION... Either this method or storeFrontHeader must be replaced.
* @example US
* @returns {String}
* @overridable
*/
Environment.prototype.storeFrontCountryCode = function storeFrontCountryCode() {};
/**
* The value contained in the X-Apple-Store-Front header value at the time the event is being created.
* NO DEFAULT IMPLEMENTATION... Either this method or storeFrontCountryCode must be replaced.
* @example K143441-1,29 ab:rSwnYxS0
* @returns {String}
* @overridable
*/
Environment.prototype.storeFrontHeader = function storeFrontHeader() {};
/**
* The best supported language for a storefront
* NO DEFAULT IMPLEMENTATION... HOWEVER: this field is optional
* @example en-US
* @returns {String}
* @overridable
*/
Environment.prototype.storeFrontLanguage = function storeFrontLanguage() {};
/**
* The type of subscriber this user is.
* NO DEFAULT IMPLEMENTATION... THIS METHOD SHOULD BE REPLACED
* @example subscribed, notSubscribed, unknown, needsAuthentication
* @returns {String}
* @overridable
*/
Environment.prototype.userType = function userType() {};
/*
* src/event_recorder
* mt-metricskit-delegates-web
*
* Copyright © 2022 Apple Inc. All rights reserved.
*
*/
/**
* A proxy EventRecorder to bridge the delegates layer to the mt-event-queue lib
* @param eventRecorder - A eventRecorder implementation from the mt-event-queue lib
* @constructor
*/
function EventRecorder(eventRecorder) {
AbstractEventRecorder.call(this);
this._proxyEventRecorder = eventRecorder;
this.SEND_METHOD = eventRecorder.SEND_METHOD;
}
EventRecorder.prototype = Object.create(AbstractEventRecorder.prototype);
EventRecorder.prototype.constructor = EventRecorder;
/**
* recordEvent implementation
* This is an implementation for "AbstractEventRecorder._recordEvent"
* @override
* @param topic
* @param eventFields
* @private
*/
EventRecorder.prototype._recordEvent = function _recordEvent(topic, eventFields) {
return this._proxyEventRecorder.recordEvent.apply(this._proxyEventRecorder, arguments);
};
/**
* This overrides the same method in AbstractEventRecorder to ignore the pending recorded event when the appExitSendMethod === 'SEND_METHOD.BEACON_SYNCHRONOUS'.
* @param {Boolean} appIsExiting - Pass true if events are being flushed due to your app exiting or page going away
* (the send method will be different in order to attempt to post events prior to actual termination)
* @param {String} appExitSendMethod (optional) the send method for how events will be flushed when the app is exiting.
* Possible options are enumerated in the `eventRecorder.SEND_METHOD` object.
* Note: This argument will be ignored if appIsExiting is false.
* @returns {Promise}
*/
EventRecorder.prototype.flushUnreportedEvents = function flushUnreportedEvents(appIsExiting, appExitSendMethod) {
var self = this;
var args = Array.prototype.slice.call(arguments);
// if this._proxyEventRecorder is an instance of QueuedEventRecorder and the callers wanted to flush events synchronously, ignore the pending events
if (
reflect.isDefinedNonNull(this._proxyEventRecorder.SEND_METHOD) &&
appExitSendMethod === this._proxyEventRecorder.SEND_METHOD.BEACON_SYNCHRONOUS
) {
return this._proxyEventRecorder.flushUnreportedEvents.apply(this._proxyEventRecorder, arguments);
} else {
return this._operationPromiseChain.then(function () {
// Reset the promise chain
self._operationPromiseChain = Promise.resolve();
return self._proxyEventRecorder.flushUnreportedEvents.apply(self._proxyEventRecorder, args);
});
}
};
/**
* Sends any remaining events in the queue
* This is an implementation for "AbstractEventRecorder._flushUnreportedEvents"
* @override
*/
EventRecorder.prototype._flushUnreportedEvents = function _flushUnreportedEvents() {
return this._proxyEventRecorder.flushUnreportedEvents.apply(this._proxyEventRecorder, arguments);
};
/**
* The methodology being used to send batches of events to the server
* This field should be hardcoded in the client based on what method it is using to encode and send its events to Figaro.
* The three typical values are:
* "itms" - use this value when/if JavaScript code enqueues events for sending via the "itms.recordEvent()" method in ITML.
* "itunes" - use this value when/if JavaScript code enqueues events by calling the "iTunes.recordEvent()" method in Desktop Store apps.
* "javascript" - use this value when/if JavaScript code enqueues events for sending via the JavaScript eventQueue management. This is typically only used by older clients which don't have the built-in functionality of itms or iTunes available to them.
* DEFAULT implementation: console.debug()
* @example "itms", "itunes", "javascript"
* @returns {String}
* @overridable
*/
EventRecorder.prototype.sendMethod = function sendMethod() {
return this._proxyEventRecorder.sendMethod.apply(this._proxyEventRecorder, arguments);
};
/**
* Set event queue related properties for the giving topic
* @param {String} topic defines the Figaro "topic" that this event should be stored under
* @param {Object} properties the event queue properties for the topic
* @param {Boolean} properties.anonymous true if sending all events for the topic with credentials omitted(no cookies, no PII fields)
*/
EventRecorder.prototype.setProperties = function setProperties(topic, properties) {
return this._proxyEventRecorder.setProperties.apply(this._proxyEventRecorder, arguments);
};
/**
* clean resources of event recorder
* Subclasses implement this method to handle how to clean resources
* @returns {Promise} returns a Promise if the cleanup will asynchronously execute or undefined for synchronously executing
*/
EventRecorder.prototype.cleanup = function cleanup() {
return this._proxyEventRecorder.cleanup.apply(this._proxyEventRecorder, arguments);
};
/*
* src/web_delegate.js
* mt-metricskit-delegates-web
*
* Copyright © 2022 Apple Inc. All rights reserved.
*
*/
/**
* Delegate for providing access to the "canned" web MetricsKit delegates.
* If further modification of these delegates is required, clients may pass in delegate options to override any of the fields within any delegate.
* @constructor
* @param {String} topic - Defines the AMP Analytics "topic" for events to be stored under
* @param {Object} delegateOptions (optional) - Options that can be passed to either add additional delegates or to extend/override existing ones.
*/
var WebDelegates = function WebDelegates(topic, delegateOptions) {
var config = this.getOrCreateConfig(topic);
Delegates.call(this, topic, {
environment: new Environment(config),
eventRecorder: new EventRecorder(new EventRecorder$1(config))
});
this.immediateEventRecorder = new EventRecorder(new ImmediateEventRecorder(config));
this.network = null;
this.logger = null;
if (delegateOptions) {
this.mergeDelegates(delegateOptions);
if (delegateOptions.environment) {
this.setEnvironment(delegateOptions.environment);
}
if (delegateOptions.eventRecorder) {
this.setEventRecorder(delegateOptions.eventRecorder);
}
if (delegateOptions.network) {
this.setNetwork(delegateOptions.network);
}
if (delegateOptions.logger) {
this.setLogger(delegateOptions.logger);
}
/**
* TODO: We are temporarily setting this configUrl() delegate on the config to avoid breaking changes.
* In the next major release, we will remove this and just fetch the config from
* the delegateOptions.config.url directly instead of reading it from
* this config method (defaulting to https://xp.apple.com/config/1/report/<topic>).
*/
if (delegateOptions.config) {
if (delegateOptions.config.url) {
this.config.setDelegate({
configUrl: delegateOptions.config.url
});
}
this.setConfig(delegateOptions.config);
}
}
};
/**
* Inherit from the base Delegate class
*/
WebDelegates.prototype = Object.create(Delegates.prototype);
WebDelegates.prototype.constructor = WebDelegates;
/**
* Sets the environment for the web-specific event queue as well as setting it on the delegate itself.
* @param {Object} environment
* @returns {WebDelegates}
*/
WebDelegates.prototype.setEnvironment = function (environment$1) {
environment.setDelegate(environment$1);
return Delegates.prototype.setEnvironment.call(this, environment$1);
};
/**
* Sets the network for the web-specific event queue as well as setting it on the delegate itself.
* @param {Object} network
* @returns {WebDelegates}
*/
WebDelegates.prototype.setNetwork = function (network$1) {
if (network$1) {
this.network = network$1;
network.setDelegate(network$1);
}
return this;
};
/**
* Sets the logger for the web-specific event queue as well as setting it on the delegate itself.
* @param {Object} logger
* @returns {WebDelegates}
*/
WebDelegates.prototype.setLogger = function (logger$1) {
if (logger$1) {
this.logger = logger$1;
logger.setDelegate(logger$1);
}
return this;
};
/**
* Sets the immediate event recorder for the delegate.
* Note: Immediate event recorders are specific to web delegates.
* @param {Object} immediateEventRecorder
* @returns {WebDelegates}
*/
WebDelegates.prototype.setImmediateEventRecorder = function (immediateEventRecorder) {
if (immediateEventRecorder) {
var newImmediateEventRecorder = Object.create(this.immediateEventRecorder);
Object.assign(newImmediateEventRecorder, immediateEventRecorder);
this.immediateEventRecorder = newImmediateEventRecorder;
}
return this;
};
/**
* @returns {Object} The config sources.
*/
WebDelegates.prototype.configSources = function configSources() {
var self = this;
return new Promise(function (resolve, reject) {
var onFetchSuccess = function onFetchSuccess(response) {
try {
var configObject = JSON.parse(response);
self.config.setCachedSource(configObject);
self.config.setServiceSource(configObject); // TODO: Deprecated
resolve(configObject);
} catch (error) {
onFetchFailure(error);
}
};
var onFetchFailure = function onFetchFailure(error) {
self.config.setCachedSource(self.config.cachedSource());
reject(error);
};
var configUrlPromise = Promise.resolve(self.config.configUrl());
configUrlPromise
.then(function (configUrl) {
backoff.exponentialBackoff(
self.config.network.makeAjaxRequest.bind(self.config.network, configUrl, 'GET', null),
onFetchSuccess,
onFetchFailure
);
})
.catch(onFetchFailure);
});
};
export { Environment, WebDelegates };