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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
|
import { isNothing } from "@jet/environment";
import { asInterface, asJSONData, asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, traverse, } from "../../foundation/json-parsing/server-data";
/**
* This class is used as a singleton to track whether we should use the crossfire attribution from the JS purchase
* configuration, this is to remedy the following issue:
*
* rdar://96291594 ([Sydney][Regression][App Store][Clickstream][iTMS11] RefApp missing from page event - (iOS) Sydney20A312a)
*
* The flow of how this object is used is as follows:
*
* 1. Deeplink comes in and is routed to the product page controller, containing the crossfire referrer data
* 2. We build the product page model and pass the referrer data along
* 3. As we're building the page model for this product page, we check to see if we have valid referrer data. If we do
* we call into `MetricsReferralContext.beginReferralContextForProduct(appProductId)`, which will create store a unique
* referral context identifier for this product page. This identifier will be used to make sure we're only every dealing
* with the original product page.
* 4. When we create the pageEvent for this product page we modify all the event fields to include unique referral context id,
* using `MetricsReferralContext.addReferralContextToMetricsFieldsIfNecessary(metricsFields)`
* 5. The referrer data is then added to the purchase configuration for the product lockup's buy button, which comes back to us
* at time of purchase decoration
* 6. Later when the user taps ont the product buy button and we enter the purchase decoration flow, we check to see if we should
* skip adding the native referrer data to the purchase buy params, which would clear out the referrer data since the page change,
* event has cleared it out natively. We call `MetricsReferralContext.shouldUseJSReferralData` for this check.
* 7. If any page event comes in to the metrics event linter, we check to see if it has a referral context id that is not the
* current one, if it does we clear the current metrics referrer contents using `MetricsReferralContext.endReferralContextIfNecessaryForPageExitEvent(pageEvent)`
* 8. Additionally in the event linter we always call into `MetricsReferralContext.removeReferralContextInfoFromMetricsEvent(metricsFields)`
* which deletes the added referralContextId field.
*/
export class MetricsReferralContext {
static createSharedMetricsReferralContext(objectGraph) {
if (MetricsReferralContext.shared) {
return;
}
MetricsReferralContext.shared = new MetricsReferralContext(objectGraph);
}
/**
* Initializers
*/
/**
* @param objectGraph The object graph to use to determine if the referral context is needed.
*/
constructor(objectGraph) {
/**
* Properties
*/
/**
* Identifier denoting that we should use the extRefUrl2 and extRefApp2 from the original purchase configuration
* found in the purchase token, rather than using the native metrics data which would clear out the ref data
* due to pageChange events.
*/
this.currentReferral = null;
if (objectGraph.host.isiOS) {
this.isMetricsReferralContextRequired = true;
this.isEventDetailClickEventOverrideNecessary = !objectGraph.host.isOSAtLeast(16, 2, 0);
}
else if (objectGraph.host.isMac) {
this.isMetricsReferralContextRequired = objectGraph.host.isOSAtLeast(13, 0, 0);
this.isEventDetailClickEventOverrideNecessary = false;
}
else {
this.isMetricsReferralContextRequired = false;
this.isEventDetailClickEventOverrideNecessary = false;
}
}
/**
* Returns whether we should use the JS referral data or not.
*/
get shouldUseJSReferralData() {
return this.isMetricsReferralContextRequired && isDefinedNonNull(this.currentReferral);
}
/**
* Returns the current referral data for the active context
*/
get activeReferralData() {
if (!this.shouldUseJSReferralData) {
return null;
}
if (this.currentReferral === null || !this.currentReferral.isActive) {
return null;
}
return this.currentReferral.data;
}
/**
* Setting Referral Data
*/
/**
* Called when we get a deep link into the product page and need to make sure we track
* the referral data for this page.
*
* @param productId The id of the product the referral context is for.
* @param referrerData The referral data for this product page.
*/
setReferralDataForProduct(productId, referrerData) {
var _a, _b, _c;
if (!this.isMetricsReferralContextRequired || isNull(referrerData)) {
return;
}
const extRefApp2 = (_a = asString(referrerData, "app")) !== null && _a !== void 0 ? _a : null;
const extRefUrl2 = (_b = asString(referrerData, "externalUrl")) !== null && _b !== void 0 ? _b : null;
const kind = (_c = asInterface(referrerData, "kind")) !== null && _c !== void 0 ? _c : null;
this.currentReferral = {
id: `${productId}_${Date.now()}`,
data: {
extRefApp2,
extRefUrl2,
kind,
refUrl: null,
},
isActive: false,
productPageExtensionInfo: null,
};
}
/**
* Called when we are linting a page event, if this page event is for a product page extension, and
* we don't yet have an active crossfire referral context, we need to make sure we start one, since there
* should always be one in this case. The fact that there is not means that the pageChange event cleared it out
* natively.
*
* Additionally we're going to add the referral context id to the page event so we can track it later, and know
* when to end the referral context.
*
* @param pageEvent The page event that is currently being linted, so we can check to see if we're on a product page,
* in the product page extension.
*/
setReferralDataForProductPageExtensionIfNecessary(pageEvent) {
var _a, _b;
if (!this.isMetricsReferralContextRequired) {
return;
}
const productId = asString(pageEvent, "pageId");
const refApp = asString(pageEvent, "refApp");
if (!MetricsReferralContextUtil.isProductPageExtension(pageEvent) ||
!MetricsReferralContextUtil.isValidPageEvent(pageEvent) ||
isNull(productId) ||
isNull(refApp)) {
return;
}
const extRefUrl = (_a = asString(pageEvent, "extRefUrl")) !== null && _a !== void 0 ? _a : null;
const refAppKindName = asString(pageEvent, "refAppType");
let refAppKindContext;
switch (refAppKindName) {
case "trampoline":
refAppKindContext = asJSONData(traverse(pageEvent, "trampolineContext"));
break;
case "widget":
refAppKindContext = asJSONData(traverse(pageEvent, "widgetContext"));
break;
default:
refAppKindContext = {};
}
const refUrl = (_b = asString(pageEvent, "refUrl")) !== null && _b !== void 0 ? _b : null;
this.currentReferral = {
id: `${productId}_${Date.now()}`,
data: {
extRefApp2: refApp,
extRefUrl2: extRefUrl,
refUrl: refUrl,
kind: {
name: refAppKindName,
context: refAppKindContext,
},
},
isActive: false,
productPageExtensionInfo: {
productId,
},
};
this.addReferralContextToMetricsFieldsIfNecessary(pageEvent);
}
/**
* Begin / End Metrics Referral Context
*/
/**
* Called when we get a deep link into the product page and need to make sure we track
* whether the referral data should be used from the js configuration.
*
* @param pageEvent Some page event that may be associated with the current referral context.
*/
beginReferralContextForPageIfNecessary(pageEvent) {
if (!this.isMetricsReferralContextRequired || !MetricsReferralContextUtil.isValidPageEvent(pageEvent)) {
return;
}
if (!MetricsReferralContextUtil.isReferralForEvent(this.currentReferral, pageEvent)) {
return;
}
if (this.currentReferral !== null) {
this.currentReferral.isActive = true;
}
}
/**
* Called when we get a pageExit event after the page event for the current deeplinked
* product page, if there is one. This will reset the flag to use the native metrics. This should
* always be the next pageExit event after the page enter event
*/
endReferralContextIfNecessaryForPageEvent(pageExitEvent) {
if (!this.isMetricsReferralContextRequired || !MetricsReferralContextUtil.isValidPageEvent(pageExitEvent)) {
return;
}
if (!MetricsReferralContextUtil.isReferralForEvent(this.currentReferral, pageExitEvent)) {
return;
}
this.currentReferral = null;
}
/**
* Setting / Clearing Page Fields
*/
/**
* Called when we're building the metrics events for a product page, this way we can tag the events with the current
* referral context id if there is one.
*
* @param pageMetricsFields The page event fields we can modify to track the current product page.
*/
addReferralContextToMetricsFieldsIfNecessary(pageMetricsFields) {
var _a;
if (!this.isMetricsReferralContextRequired) {
return;
}
pageMetricsFields[MetricsReferralContext.referralContextEventField] = (_a = this.currentReferral) === null || _a === void 0 ? void 0 : _a.id;
}
/**
* Called when linting our metrics events so we can make sure to remove the referral context id, so its not sent to the server
*
* @param metricsEvent The metrics event we're currently linting.
*/
removeReferralContextInfoFromMetricsEvent(metricsEvent) {
if (!this.isMetricsReferralContextRequired) {
return;
}
delete metricsEvent[MetricsReferralContext.referralContextEventField];
}
/**
* Event Attribution
*/
/**
* If we have an active referral context, we need to make sure we add the referral data to the event.
*
* @param metricsEvent The metrics event we're currently linting.
*/
addReferralDataToEventIfNecessary(metricsEvent) {
if (isNull(this.activeReferralData)) {
return;
}
if (!MetricsReferralContextUtil.shouldAddReferralDataToEvent(metricsEvent)) {
return;
}
if (MetricsReferralContextUtil.isEventDetailsClickEvent(metricsEvent) &&
!this.isEventDetailClickEventOverrideNecessary) {
return;
}
if (MetricsReferralContextUtil.isEventDetailsClickEvent(metricsEvent)) {
// Correct the `pageType` of this event for rdar://101302008 ([Sydney][App Store] [Clickstream][iTMS11] click event on EventDetails page from an app referral has incorrect pageType)
// Then continue on and apply referral data.
metricsEvent["pageType"] = "EventDetails";
}
metricsEvent["refApp"] = this.activeReferralData.extRefApp2;
metricsEvent["extRefUrl"] = this.activeReferralData.extRefUrl2;
if (isDefinedNonNullNonEmpty(this.activeReferralData.refUrl)) {
metricsEvent["refUrl"] = this.activeReferralData.refUrl;
}
if (this.activeReferralData !== null && this.activeReferralData.kind !== null) {
metricsEvent["refAppType"] = this.activeReferralData.kind.name;
switch (metricsEvent["refAppType"]) {
case "trampoline":
metricsEvent["trampolineContext"] = this.activeReferralData.kind.context;
break;
case "widget":
metricsEvent["widgetContext"] = this.activeReferralData.kind.context;
break;
default:
break;
}
}
}
}
/**
* They event field to use on a page event so we can determine later if this page event belongs to
* the same deeplinked product page.
*/
MetricsReferralContext.referralContextEventField = "referralContextId";
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
class MetricsReferralContextUtil {
/**
* Check to see if the pageEvent is within the ProductPageExtension
*
* @param pageEvent The page event we're checking to see if its in an extension.
* @returns Whether "app" for this event is a valid type
*/
static isProductPageExtension(pageEvent) {
const app = asString(pageEvent, "app");
return app === MetricsReferralContextUtil.productPageExtensionAppId;
}
/**
* Check to see if the current page event is for a product page.
*
* @param pageEvent The page event we're checking to see if its a product page.
* @returns Whether "pageType" for this event is a valid type
*/
static isValidPageEvent(pageEvent) {
const pageType = asString(pageEvent, "pageType");
if (isNothing(pageType)) {
return false;
}
return MetricsReferralContextUtil.validPageEventTypes.has(pageType);
}
/**
* This method will check the `referralContextEventField` to see if it matches the current referral.
*
* @param referral The current metrics referral taken from the referral context
* @param event Some event to test whether there is an associated referral context. And if so
* if that referral context matches.
*/
static isReferralForEvent(referral, event) {
var _a;
if (isNull(referral)) {
return false;
}
const referralContextId = event[MetricsReferralContext.referralContextEventField];
const productId = asString(event, "pageId");
if (isDefinedNonNull(referralContextId)) {
return referralContextId === referral.id;
}
else if (MetricsReferralContextUtil.isProductPageExtension(event) && isDefinedNonNull(productId)) {
// For product page extensions we do not get a chance to add the referralContextId to the
// pageExit event so we need to check the productId to see if it matches the current referral.
return productId === ((_a = referral === null || referral === void 0 ? void 0 : referral.productPageExtensionInfo) === null || _a === void 0 ? void 0 : _a.productId);
}
else {
return false;
}
}
static shouldAddReferralDataToEvent(event) {
// Generally, we don't want to force referral data onto click events, but this is not true for In-App Events (IAE):
// rdar://101399254 ([Sydney] [App Store][Clickstream][iTMS11] Missing refApp and extRefURL on IAE Click Open events through App/Web Referrals)
// This only applies prior to SydneyC, as this was fixed natively there.
if (event.eventType === "click") {
return this.isEventDetailsClickEvent(event);
}
return true;
}
/**
* Check whether the event is for a click on an In-App Events (IAE) page.
*
* @param event The event we're checking to see if it's on a an IAE page.
*/
static isEventDetailsClickEvent(event) {
if (event.eventType !== "click") {
return false;
}
const location = event.location;
const currentLocation = location === null || location === void 0 ? void 0 : location[0];
return isDefinedNonNull(currentLocation) && currentLocation.locationType === "EventDetails";
}
}
/**
* The identifier for the product page extension in a metrics page event
*/
MetricsReferralContextUtil.productPageExtensionAppId = "com.apple.AppStore.ProductPageExtension";
/**
* The identifier used for the pageType field of an app event page event.
*/
MetricsReferralContextUtil.eventDetailsPageType = "EventDetails";
/**
* The set of valid page types for a product page, page event
*/
MetricsReferralContextUtil.validPageEventTypes = new Set([
"Software",
"SoftwareBundle",
MetricsReferralContextUtil.eventDetailsPageType,
]);
//# sourceMappingURL=metrics-referral-context.js.map
|