-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
extension-analytics.js
235 lines (217 loc) · 6.99 KB
/
extension-analytics.js
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
import {CommonSignals_Enum} from '#core/constants/common-signals';
import {createElementWithAttributes, removeElement} from '#core/dom';
import {isArray} from '#core/types';
import {getWin} from '#core/window';
import {Services} from '#service';
import {triggerAnalyticsEvent} from '#utils/analytics';
import {devAssert} from '#utils/log';
/**
* Method to create scoped analytics element for any element.
* TODO: Make this function private
* @param {!Element} parentElement
* @param {!JsonObject} config
* @param {boolean=} loadAnalytics
* @param {boolean=} disableImmediate
* @return {!Element} created analytics element
*/
export function insertAnalyticsElement(
parentElement,
config,
loadAnalytics = false,
disableImmediate = false
) {
const doc = /** @type {!Document} */ (parentElement.ownerDocument);
const analyticsElem = createElementWithAttributes(doc, 'amp-analytics', {
'sandbox': 'true',
'trigger': disableImmediate ? '' : 'immediate',
});
const scriptElem = createElementWithAttributes(doc, 'script', {
'type': 'application/json',
});
scriptElem.textContent = JSON.stringify(config);
analyticsElem.appendChild(scriptElem);
analyticsElem.CONFIG = config;
// Force load analytics extension if script not included in page.
if (loadAnalytics) {
// Get Extensions service and force load analytics extension.
const extensions = Services.extensionsFor(getWin(parentElement));
const ampdoc = Services.ampdoc(parentElement);
extensions./*OK*/ installExtensionForDoc(ampdoc, 'amp-analytics');
} else {
Services.analyticsForDocOrNull(parentElement).then((analytics) => {
devAssert(analytics);
});
}
parentElement.appendChild(analyticsElem);
return analyticsElem;
}
/**
* A class that handles customEvent reporting of extension element through
* amp-analytics. This class is not exposed to extension element directly to
* restrict the genration of the config Please use CustomEventReporterBuilder to
* build a CustomEventReporter instance.
*/
class CustomEventReporter {
/**
* @param {!Element} parent
* @param {!JsonObject} config
*/
constructor(parent, config) {
devAssert(config['triggers'], 'Config must have triggers defined');
/** @private {string} */
this.id_ = parent.getResourceId();
/** @private {!AmpElement} */
this.parent_ = parent;
/** @private {JsonObject} */
this.config_ = config;
for (const event in config['triggers']) {
const eventType = config['triggers'][event]['on'];
devAssert(
eventType,
'CustomEventReporter config must specify trigger eventType'
);
const newEventType = this.getEventTypeInSandbox_(eventType);
config['triggers'][event]['on'] = newEventType;
}
this.parent_
.signals()
.whenSignal(CommonSignals_Enum.LOAD_START)
.then(() => {
insertAnalyticsElement(this.parent_, config, true);
});
}
/**
* @param {string} eventType
* @param {!JsonObject=} opt_vars A map of vars and their values.
*/
trigger(eventType, opt_vars) {
devAssert(
this.config_['triggers'][eventType],
'Cannot trigger non initiated eventType'
);
triggerAnalyticsEvent(
this.parent_,
this.getEventTypeInSandbox_(eventType),
opt_vars,
/** enableDataVars */ false
);
}
/**
* @param {string} eventType
* @return {string}
*/
getEventTypeInSandbox_(eventType) {
return `sandbox-${this.id_}-${eventType}`;
}
}
/**
* A builder class that enable extension elements to easily build and get a
* CustomEventReporter instance. Its constructor requires the parent AMP
* element. It provides two methods #track() and #build() to build the
* CustomEventReporter instance.
*/
export class CustomEventReporterBuilder {
/** @param {!AmpElement} parent */
constructor(parent) {
/** @private {!AmpElement} */
this.parent_ = parent;
/** @private {?JsonObject} */
this.config_ = /** @type {JsonObject} */ ({
'requests': {},
'triggers': {},
});
}
/**
* @param {!JsonObject} transportConfig
*/
setTransportConfig(transportConfig) {
this.config_['transport'] = transportConfig;
}
/**
* @param {!JsonObject} extraUrlParamsConfig
*/
setExtraUrlParams(extraUrlParamsConfig) {
this.config_['extraUrlParams'] = extraUrlParamsConfig;
}
/**
* The #track() method takes in a unique custom-event name, and the
* corresponding request url (or an array of request urls). One can call
* #track() multiple times with different eventType name (order doesn't
* matter) before #build() is called.
* @param {string} eventType
* @param {string|!Array<string>} request
* @return {!CustomEventReporterBuilder}
*/
track(eventType, request) {
request = isArray(request) ? request : [request];
devAssert(
!this.config_['triggers'][eventType],
'customEventReporterBuilder should not track same eventType twice'
);
const requestList = [];
for (let i = 0; i < request.length; i++) {
const requestName = `${eventType}-request-${i}`;
this.config_['requests'][requestName] = request[i];
requestList.push(requestName);
}
this.config_['triggers'][eventType] = {
'on': eventType,
'request': requestList,
};
return this;
}
/**
* Call the #build() method to build and get the CustomEventReporter instance.
* One CustomEventReporterBuilder instance can only build one reporter, which
* means #build() should only be called once after all eventType are added.
* @return {!CustomEventReporter}
*/
build() {
devAssert(this.config_, 'CustomEventReporter already built');
const report = new CustomEventReporter(
this.parent_,
/** @type {!JsonObject} */ (this.config_)
);
this.config_ = null;
return report;
}
}
/**
* A helper method that should be used by all extension elements to add their
* sandbox analytics tracking. This method takes care of insert and remove the
* analytics tracker at the right time of the element lifecycle.
* @param {!AmpElement} element
* @param {!Promise<!JsonObject>} promise
*/
export function useAnalyticsInSandbox(element, promise) {
let analyticsElement = null;
let configPromise = promise;
// Listener to LOAD_START signal. Insert analytics element on LOAD_START
element
.signals()
.whenSignal(CommonSignals_Enum.LOAD_START)
.then(() => {
if (analyticsElement || !configPromise) {
return;
}
configPromise.then((config) => {
if (!configPromise) {
// If config promise resolve after unload, do nothing.
return;
}
configPromise = null;
analyticsElement = insertAnalyticsElement(element, config, false);
});
});
// Listener to UNLOAD signal. Destroy remove element on UNLOAD
element
.signals()
.whenSignal(CommonSignals_Enum.UNLOAD)
.then(() => {
configPromise = null;
if (analyticsElement) {
removeElement(analyticsElement);
analyticsElement = null;
}
});
}