Skip to content

Commit 5862f94

Browse files
ninacebanNina Ciocanu
and
Nina Ciocanu
authored
Sandbox app: A4T client side logging example (#797)
* a reference implementation on how to catch analytics token from triggered events Co-authored-by: Nina Ciocanu <nciocanu@adobe.com>
1 parent cd1a41c commit 5862f94

8 files changed

+372
-3
lines changed

sandbox/public/index.html

+23-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script>
1313

1414
<title>Mock website hosting Alloy</title>
15+
<script src="/onBeforeEventSentHelper.js"></script>
1516

1617
<!-- prettier-ignore -->
1718
<script nonce="%REACT_APP_NONCE%">
@@ -102,10 +103,30 @@
102103
// For Testing multiple instances.
103104
// We use a different orgId and edgeConfigId.
104105
organizationTwo("configure", {
105-
edgeConfigId: "9999999",
106+
edgeConfigId: "7984963a-6609-4e84-98d5-4e2ff8c0dd5e:prod",
106107
orgId: "97D1F3F459CE0AD80A495CBE@AdobeOrg",
107108
debugEnabled: true,
108-
clickCollectionEnabled: false
109+
clickCollectionEnabled: false,
110+
onBeforeEventSend: function(options) {
111+
const xdm = options.xdm;
112+
const eventType = xdm.eventType;
113+
if (eventType === "decisioning.propositionInteract") {
114+
const analyticsPayloads = new Set();
115+
const propositions = xdm._experience.decisioning.propositions;
116+
117+
for (var i = 0; i < propositions.length; i++) {
118+
var proposition = propositions[i];
119+
analyticsPayloads.add(getAnalyticsPayload(proposition));
120+
}
121+
122+
getECID("organizationTwo").then(visitorID => {
123+
const analyticsPayload = concatenateAnalyticsPayloads(
124+
analyticsPayloads
125+
);
126+
sendAnalyticsPayload({ analyticsPayload, visitorID });
127+
});
128+
}
129+
}
109130
});
110131
</script>
111132
</head>
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// code to collect tnta and trigger the Analytics Hit
2+
const trackingServer = "ujsl.sc.omtrdc.net";
3+
const reportSuite = "ujslecommerce";
4+
5+
function sendAnalyticsPayload({ analyticsPayload, visitorID }) {
6+
const url = `https://${trackingServer}/b/ss/${reportSuite}/0?g=${window.location}&r=${document.referrer}&mid=${visitorID}&tnta=${analyticsPayload}`;
7+
8+
return fetch(url)
9+
.then(success => {
10+
console.log("success", success);
11+
})
12+
.catch(error => {
13+
console.log("error while triggering Analytics hit", error);
14+
});
15+
}
16+
17+
function getAnalyticsPayload(proposition) {
18+
const { scopeDetails = {} } = proposition;
19+
const { characteristics = {} } = scopeDetails;
20+
const { analyticsToken } = characteristics;
21+
22+
if (analyticsToken === undefined) {
23+
return;
24+
}
25+
return analyticsToken;
26+
}
27+
const concatenateAnalyticsPayloads = analyticsPayloads => {
28+
if (analyticsPayloads.size > 1) {
29+
return [...analyticsPayloads].join(",");
30+
}
31+
return [...analyticsPayloads].join();
32+
};
33+
34+
const getECID = instanceName => {
35+
return window[instanceName]("getIdentity", { namespaces: ["ECID"] }).then(
36+
result => {
37+
return result.identity.ECID;
38+
}
39+
);
40+
};

sandbox/src/App.js

+10
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import OrgTwo from "./OrgTwo";
2525
import DualTag from "./DualTag";
2626
import RedirectOffers from "./RedirectOffers";
2727
import RedirectedNewPage from "./RedirectedNewPage";
28+
import PersonalizationAnalyticsClientSide from "./PersonalizationAnalyticsClientSide";
2829

2930
function BasicExample() {
3031
return (
@@ -43,6 +44,11 @@ function BasicExample() {
4344
<li>
4445
<Link to="/personalizationSpa">Personalization - SPA</Link>
4546
</li>
47+
<li>
48+
<Link to="/personalizationA4TClientSide">
49+
Personalization - A4T Client Side
50+
</Link>
51+
</li>
4652
<li>
4753
<Link to="/personalizationProfile">Personalization - Profile</Link>
4854
</li>
@@ -76,6 +82,10 @@ function BasicExample() {
7682
<Route path="/consent" component={Consent} />
7783
<Route path="/personalization" component={Personalization} />
7884
<Route path="/personalizationSpa" component={PersonalizationSpa} />
85+
<Route
86+
path="/personalizationA4TClientSide"
87+
component={PersonalizationAnalyticsClientSide}
88+
/>
7989
<Route
8090
path="/personalizationProfile"
8191
component={PersonalizationProfile}

sandbox/src/DataInsertionAPI.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const trackingServer = "ujsl.sc.omtrdc.net";
2+
const reportSuite = "ujslecommerce";
3+
4+
export function sendAnalyticsPayload({ analyticsPayload, visitorID }) {
5+
const url = `https://${trackingServer}/b/ss/${reportSuite}/0?g=${window.location}&r=${document.referrer}&mid=${visitorID}&tnta=${analyticsPayload}`;
6+
7+
return fetch(url)
8+
.then(success => {
9+
console.log("success", success);
10+
})
11+
.catch(error => {
12+
console.warn("error while triggering Analytics hit", error);
13+
});
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React, { useEffect } from "react";
2+
import ContentSecurityPolicy from "./components/ContentSecurityPolicy";
3+
import { Link, Route, Switch, useRouteMatch } from "react-router-dom";
4+
import {
5+
getFormBasedOffer,
6+
personalizationEvent
7+
} from "./personalizationAnalyticsClientSideHelper";
8+
9+
const Products = () => {
10+
personalizationEvent({ renderDecisions: true });
11+
return (
12+
<div>
13+
<h2>Products</h2>
14+
<div
15+
style={{ border: "1px solid red" }}
16+
id="personalization-products-container"
17+
>
18+
This is the personalization placeholder for the products view.
19+
Personalized content has not been loaded.
20+
</div>
21+
</div>
22+
);
23+
};
24+
25+
const Cart = () => {
26+
personalizationEvent({ renderDecisions: true });
27+
return (
28+
<div>
29+
<h2>Cart</h2>
30+
<div
31+
style={{ border: "1px solid red" }}
32+
id="personalization-cart-container"
33+
>
34+
This is the personalization placeholder for the cart view. Personalized
35+
content has not been loaded.
36+
</div>
37+
</div>
38+
);
39+
};
40+
41+
export default function PersonalizationAnalyticsClientSide() {
42+
useEffect(() => {
43+
personalizationEvent({ renderDecisions: true });
44+
}, []);
45+
46+
const match = useRouteMatch();
47+
48+
return (
49+
<div>
50+
<ContentSecurityPolicy />
51+
<h1>Personalization with A4T client side logging</h1>
52+
<p>
53+
This page tests rendering of activities using a <i>__view__</i> scope,
54+
collecting the analyticsTokens from the rendered propositions and
55+
trigger a Analytics hit using Data Insertion API. Important!!! If you
56+
navigated here from another sandbox view, you will probably need to
57+
refresh your browser because this is how to properly simulate a non-SPA
58+
workflow.
59+
</p>
60+
<div style={{ border: "1px solid red" }} id="personalization-container">
61+
This is the personalization placeholder. Personalized content has not
62+
been loaded.
63+
</div>
64+
<div>
65+
<p>To retrieve a form based composed offer click on this button:</p>
66+
<button onClick={getFormBasedOffer}>
67+
Get a4t-test-scope location offer
68+
</button>
69+
70+
<div
71+
style={{ border: "1px solid red", margin: "10px 0 10px 0" }}
72+
id="form-based-offer-container"
73+
>
74+
This is the personalization placeholder for a form based composed
75+
offer. Personalized content has not been loaded.
76+
</div>
77+
78+
<button
79+
style={{ margin: "10px 0 10px 0" }}
80+
id="form-based-click-metric"
81+
>
82+
{" "}
83+
Click me!
84+
</button>
85+
</div>
86+
<p> This section is to simulate a SPA use case. </p>
87+
<ul>
88+
<li>
89+
<Link to={`${match.url}/products`}>Products</Link>
90+
</li>
91+
<li>
92+
<Link to={`${match.url}/cart`}>Cart</Link>
93+
</li>
94+
</ul>
95+
<Switch>
96+
<Route path={`${match.path}/products`}>
97+
<Products />
98+
</Route>
99+
<Route path={`${match.path}/cart`}>
100+
<Cart />
101+
</Route>
102+
</Switch>
103+
</div>
104+
);
105+
}

sandbox/src/analyticsTokenHandler.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
export function getAnalyticsToken(proposition) {
2+
const { scopeDetails = {} } = proposition;
3+
const { characteristics = {} } = scopeDetails;
4+
const { analyticsToken } = characteristics;
5+
6+
if (analyticsToken === undefined) {
7+
return;
8+
}
9+
return analyticsToken;
10+
}
11+
12+
export const concatenateAnalyticsPayloads = analyticsPayloads => {
13+
if (analyticsPayloads.size > 1) {
14+
return [...analyticsPayloads].join(",");
15+
}
16+
return [...analyticsPayloads].join();
17+
};
18+
19+
export const collectAnalyticsPayloadData = propositions => {
20+
const analyticsPayloads = new Set();
21+
22+
propositions.forEach(proposition => {
23+
const { renderAttempted = false } = proposition;
24+
25+
if (renderAttempted !== true) {
26+
return;
27+
}
28+
29+
const analyticsPayload = getAnalyticsToken(proposition);
30+
31+
if (analyticsPayload === undefined) {
32+
return;
33+
}
34+
35+
analyticsPayloads.add(analyticsPayload);
36+
});
37+
38+
return concatenateAnalyticsPayloads(analyticsPayloads);
39+
};
40+
41+
export const getECID = instanceName => {
42+
return window[instanceName]("getIdentity", { namespaces: ["ECID"] }).then(
43+
result => {
44+
return result.identity.ECID;
45+
}
46+
);
47+
};

sandbox/src/components/ContentSecurityPolicy.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ export default function ContentSecurityPolicy() {
1919
<meta
2020
http-equiv="Content-Security-Policy"
2121
// cdn.tt.omtrdc.net is necessary for Target VEC to function properly.
22+
// *.sc.omtrdc.net is necessary for Analytics Data Insertion API to function properly
2223
content={`default-src 'self';
2324
script-src 'self' 'nonce-${process.env.REACT_APP_NONCE}' cdn.jsdelivr.net assets.adobedtm.com cdn.tt.omtrdc.net;
2425
style-src 'self' 'unsafe-inline';
2526
img-src * data:;
26-
connect-src 'self' *.alloyio.com *.adobedc.net *.demdex.net`}
27+
connect-src 'self' *.alloyio.com *.adobedc.net *.demdex.net *.sc.omtrdc.net`}
2728
/>
2829
</Helmet>
2930
);

0 commit comments

Comments
 (0)