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 () () " * 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 // map the browser identifiers to readable string. For example, Edg -> Edge, OPR -> Opera * } * @example Safari * @returns {Promise | 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: (?...) 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; }; /** * Client’s 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: 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/). */ 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 };