1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
|
import { MetricsConfig } from '@amp-metrics/mt-client-config';
import { reflect } from '@amp-metrics/mt-metricskit-utils-private';
/*
* src/delegate.js
* mt-metricskit-delegates-core
*
* Copyright © 2022 Apple Inc. All rights reserved.
*
*/
/**
* Abstract class for delegates.
* All delegate implementations should extend from this class.
* @param {String} topic - Defines the AMP Analytics "topic" for events to be stored under
* @param {Object} platformImpls - A map that include the platform-based components.
* @param {Environment} platformImpls.environment - An Environment that provide the event field accessor for the platform
* @param {EventRecorder} platformImpls.eventRecorder - An EventRecorder that provide the EventRecorder implementation for the platform
* @constructor
*/
var Delegates = function Delegates(topic, platformImpls) {
if (!reflect.isDefinedNonNullNonEmpty(topic) || !reflect.isString(topic)) {
throw new Error('No valid topic was provided to Delegates.');
}
this.config = this.getOrCreateConfig(topic);
// TODO change platformImpls as an required argument in the next major version or supporting Typescript
// since every platforms should provide their own impl for the Environment and EventHandler to the Delegates.
if (reflect.isDefinedNonNull(platformImpls)) {
this.environment = platformImpls.environment;
this.eventRecorder = platformImpls.eventRecorder;
}
// A flag to indicate whether using the original execution context when calling the delegate methods. Default is false
// TODO Consider to change this to true by default in the next major release
// because using the execution context of the delegate methods may not necessary as accessing the the delegate methods' execution context is straightforward.
this._useOrginalContextForDelegateFunc = false;
};
/**
* Initializes the delegate by setting up the config.
* @param delegates
* @return {Promise}
*/
Delegates.prototype.init = function () {
if (!reflect.isDefinedNonNull(this.environment)) {
throw new Error('No environment was provided to Delegate options.');
}
if (!reflect.isDefinedNonNull(this.eventRecorder)) {
throw new Error('No eventRecorder was provided to Delegate options.');
}
this.config.environment.setDelegate(this.environment);
this.config.logger.setDelegate(this.logger);
this.config.network.setDelegate(this.network);
var configSources = reflect.isFunction(this.configSources) ? this.configSources.bind(this) : null;
return this.config.init(configSources);
};
/**
* Merge the provided delegates into the current Delegate.
* NOTE: Any delegates which already exist in the Delegate won't be merged.
* @param {Object} delegates - A key/value map of the system delegates
*/
Delegates.prototype.mergeDelegates = function (delegates) {
var self = this;
for (var key in delegates) {
if (!self[key]) {
self[key] = delegates[key];
}
}
this.config.setDelegate(delegates.config);
};
/**
* Cleans up resources used by the delegate.
*/
Delegates.prototype.cleanup = function cleanup() {
if (reflect.isFunction(this.eventRecorder.cleanup)) {
this.eventRecorder.cleanup();
}
this.config = null;
this.eventRecorder = null;
this.environment = null;
};
/**
* Overrides the platform-specific event recorder for the Delegate.
* @param {object} eventRecorderDelegates
* @returns {Delegates}
*/
Delegates.prototype.setEventRecorder = function setEventRecorder(eventRecorderDelegates) {
if (reflect.isDefinedNonNull(eventRecorderDelegates)) {
if (!reflect.isDefinedNonNull(this.eventRecorder)) {
this.eventRecorder = eventRecorderDelegates;
} else {
reflect.setDelegates(this.eventRecorder, eventRecorderDelegates);
this.eventRecorder.setDelegate(eventRecorderDelegates);
}
}
return this;
};
/**
* Overrides the existing environment implementations.
* @param {object} environment
* @return {Delegates}
*/
Delegates.prototype.setEnvironment = function setEnvironment(environment) {
if (!reflect.isDefinedNonNull(this.environment)) {
this.environment = environment;
} else {
var newEnvironment = Object.create(this.environment);
reflect.extend(newEnvironment, environment);
this.environment = newEnvironment;
}
return this;
};
/**
* Overrides the config methods
* @param config
* @return {Delegates}
*/
Delegates.prototype.setConfig = function setConfig(config) {
this.config.setDelegate(config);
return this;
};
/**
* Access the config instance from the child delegates before/during calling Delegates.constructor,
* this method will create new config instance and set it to the Delegates instance and return the config when the config doesn't exist
* @param {String} topic - The topic to create the metrics config instance
* @return {MetricsConfig}
*/
Delegates.prototype.getOrCreateConfig = function getOrCreateConfig(topic) {
if (!reflect.isDefinedNonNull(this.config)) {
this.config = new MetricsConfig(topic);
}
return this.config;
};
/**
* Retrieves the config sources. This method must be implemented by subdelegates.
* @abstract
* @return {Promise}
*/
Delegates.prototype.configSources = function configSources() {
throw new Error('This method should be implemented by subdelegates.');
};
/*
* src/abstract_event_recorder.js
* mt-metricskit-delegates-core
*
* Copyright © 2022 Apple Inc. All rights reserved.
*
*/
/**
* An abstract event recorder to provide common logic across platform event recorders
* @constructor
*/
function AbstractEventRecorder() {
this._operationPromiseChain = Promise.resolve();
}
/**
* Allows replacement of one or more of this class' functions
* Any method on the passed-in object which matches a method that this class has will be called instead of the built-in class method.
* To replace *all* methods of his class, simply have your delegate implement all the methods of this class
* Your delegate can be a true object instance, an anonymous object, or a class object.
* Your delegate is free to have as many additional non-matching methods as it likes.
* It can even act as a delegate for multiple MetricsKit objects, though that is not recommended.
*
* "setDelegate()" may be called repeatedly, with the functions in the most-recently set delegates replacing any functions matching those in the earlier delegates, as well as any as-yet unreplaced functions.
* This allows callers to use "canned" delegates to get most of their functionality, but still replace some number of methods that need custom implementations.
* If, for example, a client wants to use the "canned" itml/environment delegate with the exception of, say, the "appVersion" method, they can set itml/environment as the delegate, and
* then call "setDelegate()" again with their own delegate containing only a single method of "appVersion" as the delegate, which would leave all the other "replaced" methods intact,
* but override the "appVersion" method again, this time with their own supplied delegate.
*
* NOTE: when the delegate function is called, it will include an additional final parameter representing the original function that it replaced (the callee would typically name this parameter "replacedFunction").
* This allows the delegate to, essentially, call "super" before or after it does some work.
* If a replaced method is overridden again with a subsequent "setDelegate()" call, the "replacedFunction" parameter will be the previous delegate.
* @example:
* To override one or more methods, in place:
* eventRecorder.setDelegate({recordEvent: itms.recordEvent});
* To override one or more methods with a separate object:
* eventRecorder.setDelegate(eventRecorderDelegate);
* (where "eventRecorderDelegate" might be defined elsewhere as, e.g.:
* var eventRecorderDelegate = {recordEvent: itms.recordEvent,
* sendMethod: 'itms'};
* To override one or more methods with an instantiated object from a class definition:
* eventRecorder.setDelegate(new EventRecorderDelegate());
* (where "EventRecorderDelegate" might be defined elsewhere as, e.g.:
* function EventRecorderDelegate() {
* }
* EventRecorderDelegate.prototype.recordEvent = itms.recordEvent;
* EventRecorderDelegate.prototype.sendMethod = function sendMethod() {
* return 'itms';
* };
* To override one or more methods with a class object (with "static" methods):
* eventRecorder.setDelegate(EventRecorderDelegate);
* (where "EventRecorderDelegate" might be defined elsewhere as, e.g.:
* function EventRecorderDelegate() {
* }
* EventRecorderDelegate.recordEvent = itms.recordEvent;
* EventRecorderDelegate.sendMethod = function sendMethod() {
* return 'itms';
* };
* @param {Object} Object or Class with delegate method(s) to be called instead of default (built-in) methods.
* @returns {Boolean} true if one or more methods on the delegate object match one or more methods on the default object,
* otherwise returns false.
*/
AbstractEventRecorder.prototype.setDelegate = function setDelegate(delegate) {
return reflect.attachDelegate(this, delegate);
};
/**
* Public method to interact with Processors to record an event
* @param {String} topic - an 'override' topic which will override the main topic.
* @param {Promise|Object} eventFields - a Promise/JavaScript object which will be converted to a JSON string and sent to AMP Analytics.
* @return {Promise}
*/
AbstractEventRecorder.prototype.recordEvent = function recordEvent(topic, eventFields) {
var vargs = Array.prototype.slice.call(arguments, 2);
var self = this;
this._operationPromiseChain = this._operationPromiseChain.then(function () {
return Promise.resolve(eventFields).then(function (eventFields) {
return self._recordEvent.apply(self, [topic, eventFields].concat(vargs));
});
});
return this._operationPromiseChain;
};
/**
* Sends any remaining events in the queue, then clears it
* This is typically called on page / app close when the JS context is about to disappear and thus we will
* not know if the events made it to the server
* @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}
*/
AbstractEventRecorder.prototype.flushUnreportedEvents = function flushUnreportedEvents() {
var args = Array.prototype.slice.call(arguments);
var self = this;
return this._operationPromiseChain.then(function () {
// Reset the promise chain
self._operationPromiseChain = Promise.resolve();
return self._flushUnreportedEvents.apply(self, args);
});
};
/**
* Abstract recordEvent method
* Subclasses implement this method to handle how to record an event
* @abstract
* @protected
* @param {String} topic - an 'override' topic which will override the main topic.
* @param {Object} eventFields - a JavaScript object which will be converted to a JSON string and sent to AMP Analytics.
* @returns {Promise}
*/
AbstractEventRecorder.prototype._recordEvent = function _recordEvent(topic, eventFields) {};
/**
* Abstract flushUnreportedEvents method
* Subclasses implement this method to handle how to flush the cache events
* @abstract
* @protected
* @param {Boolean} appIsExiting - if events are being flushed due to your app exiting (or page going away for web-apps), pass "true".
* This allows MetricsKit to modify its flush strategy to attempt to post events prior to actual termination.
* In cases where appIsExiting==false, the parameter may be omitted.
* @returns {Promise}
*/
AbstractEventRecorder.prototype._flushUnreportedEvents = function _flushUnreportedEvents(appIsExiting) {};
export { AbstractEventRecorder, Delegates };
|