1SalesforceInteractions.init({
2 cookieDomain: "tableau.com",
3 consents: [
4 {
5 purpose: SalesforceInteractions.mcis.ConsentPurpose.Personalization,
6 provider: "Example Consent Manager",
7 status: SalesforceInteractions.ConsentStatus.OptIn,
8 },
9 ],
10}).then(() => {
11 const findInDataLayer = (targetAttribute) => {
12 if (!window.dataLayer) {
13 return;
14 }
15 for (let i = 0; i < window.dataLayer.length; i++) {
16 const result = SalesforceInteractions.mcis.getValueFromNestedObject(
17 "window.dataLayer[" + i + "]",
18 );
19 if (result && result[targetAttribute]) {
20 return result;
21 }
22 }
23 return;
24 };
25
26 // We set a value for this variable only once per page load and then reference it several times for the data found in the dataLayer needed for each page.
27 let pageDetails;
28 const setPageDetailsFromDataLayer = () => {
29 pageDetails = pageDetails || findInDataLayer("entityId");
30 };
31
32 /*
33 Here we will first check session storage for the islogin item containing the user's email that is associated with
34 their account. This value is set in the global event listener in the site config when a submission event occurs. On this
35 website, both successful and invalid form submissions emit a submission event, the page also is quickly reloaded
36 after a successful login.
37 In this unusual situation, we will first save the submitted data in session storage. Then when the page loads,
38 we check for the islogin value in session storage and then look for a DOM element which indicates a successful
39 login before retrieving the stored user email and sending our login event.
40 */
41 let loginEvent = sessionStorage.getItem("islogin");
42 // if there is a stored login event...
43 if (loginEvent) {
44 // wait for the logged in user dropdown to appear in the nav, signifying a successful login
45 SalesforceInteractions.DisplayUtils.pageElementLoaded(
46 "#block-public-sitewide-ui-author-profile-dropdown",
47 ).then(() => {
48 // remove the stored email from session storage
49 sessionStorage.removeItem("islogin");
50 // construct and send the Login Success event to Personalization
51 SalesforceInteractions.sendEvent({
52 interaction: { name: "Login Success" },
53 user: {
54 /*
55 The users email address provided in the login form is sent to Personalization as user.identities.emailAddress because
56 in this example, emailAddress is configured to be the default identity for the web channel.
57 */
58 identities: {
59 emailAddress: loginEvent,
60 },
61 },
62 });
63 });
64 }
65
66 const config = {
67 global: {
68 contentZones: [
69 { name: "global_popup" },
70 { name: "global_infobar" },
71 { name: "global_exit_intent" },
72 /*
73 Since the website does not have consistent selectors and structure within the Article pages,
74 this content zone will be used to add recs to the bottom of Article pages.
75 */
76 { name: "global_footer", selector: "footer.global-footer" },
77 ],
78 listeners: [
79 /*
80 Here we are listening for all submission events that happen within this document. This pattern
81 can be used to create generic form submission event handlers, or even just to consolidate them
82 to one place in the sitemap code.
83 */
84 SalesforceInteractions.listener("submit", "body", (event) => {
85 // Check the id of the event target to check for the login form we want to scrape data from
86 if (event.target.id === "login-form") {
87 // loop through login form fields...
88 for (i = 0; i < event.target.length; i++) {
89 // Find the email field by id within the event object.
90 if (event.target[i].id === "login-email") {
91 // save user email in session storage
92 sessionStorage.setItem("islogin", event.target[i].value);
93 }
94 }
95 }
96 }),
97 ],
98 },
99 pageTypeDefault: {
100 name: "default",
101 interaction: {
102 name: "Default Page",
103 },
104 },
105 pageTypes: [
106 {
107 name: "home",
108 isMatch: () => /^\/$/.test(window.location.pathname),
109 interaction: {
110 name: "Homepage",
111 },
112 contentZones: [
113 { name: "home_recs_1", selector: "section.datalocation-audience-segment-links" },
114 { name: "home_recs_2", selector: "section.datalocation-customer-stories" },
115 ],
116 },
117 {
118 name: "products_landing",
119 isMatch: () => /^\/products\/?$/.test(window.location.pathname),
120 interaction: {
121 name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject,
122 catalogObject: {
123 /*
124 Remember, Catalog IDs are case sensitive. Transforming collected values as all upper case or all lower
125 case is a common method for ensuring consistency throughout the site when creating Catalog IDs with the
126 sitemap which do not need to correspond to data in external systems.
127 */
128 type: "Category",
129 id: () =>
130 SalesforceInteractions.mcis
131 .getLastPathComponentWithoutExtension(window.location.pathname)
132 .toLowerCase(),
133 attributes: {
134 url: SalesforceInteractions.resolvers.fromCanonical(),
135 name: SalesforceInteractions.resolvers.fromMeta("og:title"),
136 },
137 },
138 },
139 contentZones: [{ name: "products_landing_hero_banner", selector: "#hero" }],
140 },
141 {
142 name: "solutions_landing",
143 isMatch: () =>
144 /^\/solutions\/?(customers|industries|departments|technologies)?$/.test(
145 window.location.pathname,
146 ),
147 interaction: {
148 name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject,
149 catalogObject: {
150 /*
151 The last parameter of each resolver function accepts a function which returns whatever the desired final value is.
152 This is very useful when you need to transform or sanitize a scraped value before sending it to Interaction studio.
153
154 You can learn more about resolver functions here: https://developer.salesforce.com/docs/marketing/personalization/guide/sitemap-implementation.html#resolvers
155
156 Below, the pathname portion of the URL is transformed into a hierarchal Category id.
157 */
158 type: "Category",
159 id: SalesforceInteractions.resolvers.fromWindow("location.pathname", (path) =>
160 path.split("/").slice(1).join("|").toLowerCase(),
161 ),
162 attributes: {
163 url: SalesforceInteractions.resolvers.fromCanonical(),
164 name: SalesforceInteractions.resolvers.fromMeta("og:title"),
165 },
166 },
167 },
168 contentZones: [{ name: "solutions_landing_hero_banner", selector: "#hero" }],
169 },
170 {
171 name: "learning_landing",
172 isMatch: () => /^\/learn\/?$/.test(window.location.pathname),
173 interaction: {
174 name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject,
175 catalogObject: {
176 type: "Category",
177 id: () =>
178 SalesforceInteractions.mcis
179 .getLastPathComponentWithoutExtension(window.location.pathname)
180 .toLowerCase(),
181 attributes: {
182 url: SalesforceInteractions.resolvers.fromCanonical(),
183 name: SalesforceInteractions.resolvers.fromMeta("og:title"),
184 },
185 },
186 },
187 contentZones: [
188 {
189 name: "learning_landing_gray_recs",
190 selector: ".entity-paragraphs-item.paragraph--type--cross-reference",
191 },
192 { name: "learning_landing_hero_banner", selector: "#hero" },
193 ],
194 },
195 {
196 name: "blog_landing",
197 isMatch: () => /^\/about\/blog\/?$/.test(window.location.pathname),
198 interaction: {
199 name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject,
200 catalogObject: {
201 type: "Category",
202 id: () =>
203 SalesforceInteractions.mcis
204 .getLastPathComponentWithoutExtension(window.location.pathname)
205 .toLowerCase(),
206 attributes: {
207 url: SalesforceInteractions.resolvers.fromCanonical(),
208 name: SalesforceInteractions.resolvers.fromMeta("og:title"),
209 },
210 },
211 },
212 contentZones: [
213 { name: "blog_landing_card_wall", selector: ".card-wall" },
214 { name: "blog_landing_hero_banner", selector: "#hero" },
215 ],
216 },
217 {
218 name: "product",
219 action: "Product",
220 isMatch: () => {
221 if (/^\/products\//.test(window.location.pathname)) {
222 /*
223 We only want to call setPageDetailsFromDataLayer() when pages which rely on scraping information from the dataLayer are actually matched.
224 Adding a conditional statement prevents this function from being called on every page on the site as Personalization resolves every
225 isMatch function to see which page type matches the current one.
226 */
227 setPageDetailsFromDataLayer();
228 return true;
229 }
230 return false;
231 },
232 interaction: {
233 name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject,
234 catalogObject: {
235 /*
236 Remember, while this site does promote products, there is no ability to purchase products or attribute
237 revenue to them from other channels which would match with user activity from the web.
238 Since the special capabilities of the Product item type will not be needed for this implementation,
239 we will instead use the Article item type, utilizing Category to tell types of Article apart.
240 */
241 type: "Article",
242 /*
243 We are using SalesforceInteractions.mcis.getValueFromNestedObject() to more easily reference data from the pageDetails
244 object after the value is set when the page matches.
245 */
246 id: () => SalesforceInteractions.mcis.getValueFromNestedObject("entityId", pageDetails),
247 attributes: {
248 name: () =>
249 SalesforceInteractions.mcis.getValueFromNestedObject("entityLabel", pageDetails),
250 url: SalesforceInteractions.resolvers.fromCanonical(),
251 imageUrl: SalesforceInteractions.resolvers.fromSelectorAttribute(
252 "div.feature-highlight__image img",
253 "src",
254 ),
255 },
256 relatedCatalogObjects: {
257 Category: SalesforceInteractions.resolvers.fromWindow("location.pathname", (path) => {
258 const categories = path.split("/").slice(1);
259 categories.pop();
260 if (categories.length === 0) {
261 const dataLayerCategories = SalesforceInteractions.mcis.getValueFromNestedObject(
262 "page.category1",
263 pageDetails,
264 );
265 return dataLayerCategories ? [dataLayerCategories.toLowerCase()] : [];
266 }
267 return [categories.join("|").toLowerCase()];
268 }),
269 ProductType: () =>
270 window.location.pathname.indexOf("add-ons") > -1 ? ["Add-On"] : ["Software"],
271 Industry: () =>
272 SalesforceInteractions.mcis.getValueFromNestedObject(
273 "dataModelFields.field_dmo_industries",
274 pageDetails,
275 ) || null,
276 Department: () =>
277 SalesforceInteractions.mcis.getValueFromNestedObject(
278 "dataModelFields.field_departments",
279 pageDetails,
280 ) || null,
281 JobRole: () =>
282 SalesforceInteractions.mcis.getValueFromNestedObject(
283 "dataModelFields.taxonomy_vocabulary_23",
284 pageDetails,
285 ) || null,
286 PageBundle: () => {
287 const pageBundle = SalesforceInteractions.mcis.getValueFromNestedObject(
288 "entityBundleNice",
289 pageDetails,
290 );
291 return pageBundle ? [pageBundle] : null;
292 },
293 Keyword: SalesforceInteractions.resolvers.fromMeta("keywords", (ele) => {
294 return ele ? ele.split(/\,\s*/) : null;
295 }),
296 },
297 },
298 },
299 listeners: [
300 SalesforceInteractions.listener(
301 "submit",
302 "#webform-submission-email-embeddable-1-add-form",
303 (event) => {
304 SalesforceInteractions.sendEvent({
305 interaction: { name: "Free Trial Download" },
306 /*
307 A content zone is provided in this event in order to allow a campaign targeting the "global_infobar"
308 content zone to be returned in the response sent back from Personalization with this request.
309 */
310 source: {
311 contentZones: ["global_infobar"],
312 },
313 });
314 },
315 ),
316 ],
317 },
318 {
319 name: "solutions",
320 isMatch: () => {
321 if (
322 /\/solutions\/(?!customers|industries|departments|technologies)/.test(
323 window.location.pathname,
324 )
325 ) {
326 setPageDetailsFromDataLayer();
327 return true;
328 }
329 return false;
330 },
331 interaction: {
332 name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject,
333 catalogObject: {
334 type: "Article",
335 id: () => SalesforceInteractions.mcis.getValueFromNestedObject("entityId", pageDetails),
336 attributes: {
337 name: () =>
338 SalesforceInteractions.mcis.getValueFromNestedObject("entityLabel", pageDetails),
339 url: SalesforceInteractions.resolvers.fromCanonical(),
340 imageUrl: SalesforceInteractions.resolvers.fromSelectorAttribute(
341 "div.feature-highlight__image img",
342 "src",
343 ),
344 },
345 relatedCatalogObjects: {
346 Category: SalesforceInteractions.resolvers.fromWindow("location.pathname", (path) => {
347 const categories = path.split("/").slice(1);
348 categories.pop();
349 return [categories.join("|").toLowerCase()];
350 }),
351 Industry: () =>
352 SalesforceInteractions.mcis.getValueFromNestedObject(
353 "dataModelFields.field_dmo_industries",
354 pageDetails,
355 ) || null,
356 Department: () =>
357 SalesforceInteractions.mcis.getValueFromNestedObject(
358 "dataModelFields.field_departments",
359 pageDetails,
360 ) || null,
361 JobRole: () =>
362 SalesforceInteractions.mcis.getValueFromNestedObject(
363 "dataModelFields.taxonomy_vocabulary_23",
364 pageDetails,
365 ) || null,
366 PageBundle: () => {
367 const pageBundle = SalesforceInteractions.mcis.getValueFromNestedObject(
368 "entityBundleNice",
369 pageDetails,
370 );
371 return pageBundle ? [pageBundle] : null;
372 },
373 },
374 },
375 },
376 },
377 {
378 name: "learning",
379 isMatch: () => {
380 if (/^\/learn\//.test(window.location.pathname)) {
381 setPageDetailsFromDataLayer();
382 return true;
383 }
384 return false;
385 },
386 interaction: {
387 name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject,
388 catalogObject: {
389 type: "Article",
390 id: () => SalesforceInteractions.mcis.getValueFromNestedObject("entityId", pageDetails),
391 attributes: {
392 name: () =>
393 SalesforceInteractions.mcis.getValueFromNestedObject("entityLabel", pageDetails),
394 url: SalesforceInteractions.resolvers.fromCanonical(),
395 imageUrl: SalesforceInteractions.resolvers.fromSelectorAttribute(
396 "div.feature-highlight__image img",
397 "src",
398 ),
399 },
400 relatedCatalogObjects: {
401 Category: SalesforceInteractions.resolvers.fromWindow("location.pathname", (path) => {
402 const categories = path.split("/").slice(1);
403 categories.pop();
404 return [categories.join("|").toLowerCase()];
405 }),
406 Industry: () =>
407 SalesforceInteractions.mcis.getValueFromNestedObject(
408 "dataModelFields.field_dmo_industries",
409 pageDetails,
410 ) || null,
411 Department: () =>
412 SalesforceInteractions.mcis.getValueFromNestedObject(
413 "dataModelFields.field_departments",
414 pageDetails,
415 ) || null,
416 JobRole: () =>
417 SalesforceInteractions.mcis.getValueFromNestedObject(
418 "dataModelFields.taxonomy_vocabulary_23",
419 pageDetails,
420 ) || null,
421 PageBundle: () => {
422 const pageBundle = SalesforceInteractions.mcis.getValueFromNestedObject(
423 "entityBundleNice",
424 pageDetails,
425 );
426 return pageBundle ? [pageBundle] : null;
427 },
428 },
429 },
430 },
431 },
432 {
433 name: "blog",
434 isMatch: () => {
435 if (/\/about\/blog\/\d+\/\d+\/.+/.test(window.location.pathname)) {
436 setPageDetailsFromDataLayer();
437 return true;
438 }
439 return false;
440 },
441 interaction: {
442 name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject,
443 catalogObject: {
444 type: "Blog",
445 id: () => SalesforceInteractions.mcis.getValueFromNestedObject("entityId", pageDetails),
446 attributes: {
447 name: () =>
448 SalesforceInteractions.mcis.getValueFromNestedObject("entityLabel", pageDetails),
449 url: SalesforceInteractions.resolvers.fromCanonical(),
450 imageUrl: SalesforceInteractions.resolvers.fromMeta("og:image"),
451 },
452 relatedCatalogObjects: {
453 Category: () => [
454 SalesforceInteractions.mcis.getValueFromNestedObject(
455 "flatTaxonomy.blog_categories",
456 pageDetails,
457 ),
458 ],
459 },
460 },
461 },
462 listeners: [
463 SalesforceInteractions.listener("submit", ".premium-access-ajax", () => {
464 const eloquaId = findInDataLayer("EloquaGuid");
465 if (eloquaId) {
466 SalesforceInteractions.sendEvent({
467 interaction: { name: "Tableau Blog Sign-up" },
468 user: { attributes: { eloquaId: eloquaId } },
469 });
470 }
471 }),
472 ],
473 contentZones: [
474 /*
475 As in this case below, content zones do not necessarily have to denote content to be replaced, they can be
476 useful for inserting template content before or after the the DOM node with the provided selector.
477 */
478 { name: "blog_text_content", selector: ".field--name-field-page-sections" },
479 ],
480 },
481 ],
482 };
483 SalesforceInteractions.initSitemap(config);
484});