Skip to content

Commit dfaccd2

Browse files
authored
change: [M3-9347] - Switch to self hosting Pendo agent in Adobe Launch (#12203)
* Load the agent from the AL script instead of CDN * Remove the Optanon consent check for launch script to handle * Try to add page view tracking on first page load * Clean up cookie stuff * Update docs * Correct typo and stray console.log * Add a try-catch * Add console log for env vars to debug * Fix numbering in docs * Fix bug - resolve the promise in useScript if the script was already loaded * Add changesets * Update test spec to include new Launch script urls
1 parent fe0ca02 commit dfaccd2

File tree

10 files changed

+88
-204
lines changed

10 files changed

+88
-204
lines changed

docs/tooling/analytics.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,20 @@ Pendo is configured in [`usePendo.js`](https://github.com/linode/manager/blob/de
1212

1313
Important notes:
1414

15-
- Pendo is only loaded if the user has enabled Performance Cookies via OneTrust *and* if a valid `PENDO_API_KEY` is configured as an environment variable. In our development, staging, and production environments, `PENDO_API_KEY` is available at build time. See **Locally Testing Page Views & Custom Events and/or Troubleshooting Pendo** for set up with local environments.
16-
- We load the Pendo agent from the CDN, rather than [self-hosting](https://support.pendo.io/hc/en-us/articles/360038969692-Self-hosting-the-Pendo-agent), and we have configured a [CNAME](https://support.pendo.io/hc/en-us/articles/360043539891-CNAME-for-Pendo).
15+
- Pendo is only loaded if a valid `PENDO_API_KEY` is configured as an environment variable. In our preview, development, staging, and production environments, `PENDO_API_KEY` is available at build time. See **Locally Testing Page Views & Custom Events and/or Troubleshooting Pendo** for set up with local environments.
16+
- We [self-host](https://support.pendo.io/hc/en-us/articles/360038969692-Self-hosting-the-Pendo-agent) and load the Pendo agent from Adobe Launch, rather than from the CDN, and we have configured a [CNAME](https://support.pendo.io/hc/en-us/articles/360043539891-CNAME-for-Pendo).
17+
- As configured by Adobe Launch, Pendo will respect OneTrust cookie preferences in development, staging, and production environments and does not check cookie preferences in preview environments. Pendo will not run on localhost:3000 because it needs a Optanon cookie with the linode.com domain for consent.
1718
- At initialization, we do string transformation on select URL patterns to **remove sensitive data**. When new URL patterns are added to Cloud Manager, verify that existing transforms remove sensitive data; if not, update the transforms.
18-
- Pendo will respect OneTrust cookie preferences in development, staging, and production environments and does not check cookie preferences in the local environment.
1919
- Pendo makes use of the existing `data-testid` properties, used in our automated testing, for tagging elements. They are more persistent and reliable than CSS properties, which are liable to change.
2020

2121
### Locally Testing Page Views & Custom Events and/or Troubleshooting Pendo
2222

2323
1. Set the `REACT_APP_PENDO_API_KEY` environment variable in `.env`.
24-
2. Use the browser tools Network tab, filter requests by "psp.cloud", and check that successful network requests have been made to load Pendo scripts (also visible in the browser tools Sources tab).
25-
3. In the browser console, type `pendo.validateEnvironment()`.
26-
4. You should see command output in the console, and it should include an `accountId` and a `visitorId` that correspond with your APIv4 account `euuid` and profile `uid`, respectively. Each page view change or custom event that fires should be visible as a request in the Network tab.
27-
5. If the console does not output the expected ids and instead outputs something like `Cookies are disabled in Pendo config. Is this expected?` in response to the above command, clear app storage with the browser tools. Once redirected back to Login, update the OneTrust cookie settings to enable cookies via "Manage Preferences" in the banner at the bottom of the screen. Log back into Cloud Manager and Pendo should load.
24+
2. Confirm the Adobe Launch script has loaded. (View it in the browser console Sources tab under the assets.adobedtm.com directory.)
25+
3. Use the browser tools Network tab, filter requests by "psp.cloud", and check that successful network requests have been made to load Pendo scripts (also visible in the browser tools Sources tab).
26+
4. In the browser console, type `pendo.validateEnvironment()`.
27+
5. You should see command output in the console, and it should include an `accountId` and a `visitorId` that correspond with your APIv4 account `euuid` and profile `uid`, respectively. Each page view change or custom event that fires should be visible as a request in the Network tab.
28+
6. If the console does not output the expected ids and instead outputs something like `Cookies are disabled in Pendo config. Is this expected?` in response to the above command, clear app storage with the browser tools. Once redirected back to Login, update the OneTrust cookie settings to enable cookies via "Manage Preferences" in the banner at the bottom of the screen. Log back into Cloud Manager and Pendo should load.
2829

2930
## Adobe Analytics
3031

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Changed
3+
---
4+
5+
Switch to self-hosting the Pendo agent with Adobe Launch ([#12203](https://github.com/linode/manager/pull/12203))
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Fixed
3+
---
4+
5+
Bug in loadScript function not resolving promise if script already existed ([#12203](https://github.com/linode/manager/pull/12203))
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Fixed
3+
---
4+
5+
Bug where first pageview of landing page was not fired in Adobe Analytics ([#12203](https://github.com/linode/manager/pull/12203))

packages/manager/cypress/e2e/core/general/analytics.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import { ui } from 'support/ui';
22

33
const ADOBE_LAUNCH_URLS = [
44
'https://assets.adobedtm.com/fcfd3580c848/15e23aa7fce2/launch-92311d9d9637-development.min.js', // New dev Launch script
5-
'https://assets.adobedtm.com/fcfd3580c848/795fdfec4a0e/launch-09b7ca9d43ad-development.min.js', // Existing dev Launch script
6-
'https://assets.adobedtm.com/fcfd3580c848/795fdfec4a0e/launch-a50be9afbe1d-staging.min.js', // Existing staging Launch script
7-
'https://assets.adobedtm.com/fcfd3580c848/795fdfec4a0e/launch-de0ca78667e7.min.js', // Existing prod Launch script
5+
'https://assets.adobedtm.com/fcfd3580c848/15e23aa7fce2/launch-5bda4b7a1db9-staging.min.js', // New staging Launch script
6+
'https://assets.adobedtm.com/fcfd3580c848/15e23aa7fce2/launch-9ea21650035a.min.js', // New prod Launch script
87
];
98

109
describe('Script loading and user interaction test', () => {

packages/manager/src/hooks/useAdobeAnalytics.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export const useAdobeAnalytics = () => {
2929
'Adobe Analytics error: Not all Adobe Launch scripts and extensions were loaded correctly; analytics cannot be sent.'
3030
);
3131
}
32+
33+
// Fire the first page view for the landing page
34+
window._satellite.track('page view', {
35+
url: window.location.pathname,
36+
});
3237
})
3338
.catch(() => {
3439
// Do nothing; a user may have analytics script requests blocked.

packages/manager/src/hooks/usePendo.ts

Lines changed: 55 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,8 @@ import { useAccount, useProfile } from '@linode/queries';
22
import { loadScript } from '@linode/utilities'; // `loadScript` from `useScript` hook
33
import React from 'react';
44

5-
import { APP_ROOT, PENDO_API_KEY } from 'src/constants';
6-
import {
7-
checkOptanonConsent,
8-
getCookie,
9-
ONE_TRUST_COOKIE_CATEGORIES,
10-
} from 'src/utilities/analytics/utils';
5+
import { ADOBE_ANALYTICS_URL, APP_ROOT, PENDO_API_KEY } from 'src/constants';
6+
import { reportException } from 'src/exceptionReporting';
117

128
declare global {
139
interface Window {
@@ -70,21 +66,8 @@ export const usePendo = () => {
7066
const accountId = getUniquePendoId(account?.euuid);
7167
const visitorId = getUniquePendoId(profile?.uid.toString());
7268

73-
const optanonCookie = getCookie('OptanonConsent');
74-
// Since OptanonConsent cookie always has a .linode.com domain, only check for consent in dev/staging/prod envs.
75-
// When running the app locally, do not try to check for OneTrust cookie consent, just enable Pendo.
76-
const hasConsentEnabled =
77-
APP_ROOT.includes('localhost') ||
78-
checkOptanonConsent(
79-
optanonCookie,
80-
ONE_TRUST_COOKIE_CATEGORIES['Performance Cookies']
81-
);
82-
83-
// This URL uses a Pendo-configured CNAME (M3-8742).
84-
const PENDO_URL = `https://content.psp.cloud.linode.com/agent/static/${PENDO_API_KEY}/pendo.js`;
85-
8669
React.useEffect(() => {
87-
if (PENDO_API_KEY && hasConsentEnabled) {
70+
if (PENDO_API_KEY && ADOBE_ANALYTICS_URL) {
8871
// Adapted Pendo install script for readability
8972
// Refer to: https://support.pendo.io/hc/en-us/articles/21362607464987-Components-of-the-install-script#01H6S2EXET8C9FGSHP08XZAE4F
9073

@@ -115,54 +98,61 @@ export const usePendo = () => {
11598
})(methodNames[index]);
11699
});
117100

118-
// Load Pendo script into the head HTML tag, then initialize Pendo with metadata
119-
loadScript(PENDO_URL, {
101+
// Ensure the Adobe Launch script is loaded, then initialize Pendo with metadata
102+
loadScript(ADOBE_ANALYTICS_URL, {
120103
location: 'head',
121104
}).then(() => {
122-
window.pendo.initialize({
123-
account: {
124-
id: accountId, // Highly recommended, required if using Pendo Feedback
125-
// name: // Optional
126-
// is_paying: // Recommended if using Pendo Feedback
127-
// monthly_value:// Recommended if using Pendo Feedback
128-
// planLevel: // Optional
129-
// planPrice: // Optional
130-
// creationDate: // Optional
131-
132-
// You can add any additional account level key-values here,
133-
// as long as it's not one of the above reserved names.
134-
},
135-
// Controls what URLs we send to Pendo. Refer to: https://agent.pendo.io/advanced/location/.
136-
location: {
137-
transforms: [
138-
{
139-
action: 'Clear',
140-
attr: 'hash',
141-
},
142-
{
143-
action: 'Clear',
144-
attr: 'search',
145-
},
146-
{
147-
action: 'Replace',
148-
attr: 'pathname',
149-
data(url: string) {
150-
return transformUrl(url);
105+
try {
106+
window.pendo.initialize({
107+
account: {
108+
id: accountId, // Highly recommended, required if using Pendo Feedback
109+
// name: // Optional
110+
// is_paying: // Recommended if using Pendo Feedback
111+
// monthly_value:// Recommended if using Pendo Feedback
112+
// planLevel: // Optional
113+
// planPrice: // Optional
114+
// creationDate: // Optional
115+
116+
// You can add any additional account level key-values here,
117+
// as long as it's not one of the above reserved names.
118+
},
119+
// Controls what URLs we send to Pendo. Refer to: https://agent.pendo.io/advanced/location/.
120+
location: {
121+
transforms: [
122+
{
123+
action: 'Clear',
124+
attr: 'hash',
125+
},
126+
{
127+
action: 'Clear',
128+
attr: 'search',
129+
},
130+
{
131+
action: 'Replace',
132+
attr: 'pathname',
133+
data(url: string) {
134+
return transformUrl(url);
135+
},
151136
},
152-
},
153-
],
154-
},
155-
visitor: {
156-
id: visitorId, // Required if user is logged in
157-
// email: // Recommended if using Pendo Feedback, or NPS Email
158-
// full_name: // Recommended if using Pendo Feedback
159-
// role: // Optional
160-
161-
// You can add any additional visitor level key-values here,
162-
// as long as it's not one of the above reserved names.
163-
},
164-
});
137+
],
138+
},
139+
visitor: {
140+
id: visitorId, // Required if user is logged in
141+
// email: // Recommended if using Pendo Feedback, or NPS Email
142+
// full_name: // Recommended if using Pendo Feedback
143+
// role: // Optional
144+
145+
// You can add any additional visitor level key-values here,
146+
// as long as it's not one of the above reserved names.
147+
},
148+
});
149+
} catch (error) {
150+
reportException(
151+
'An error occurred when trying to initialize Pendo.',
152+
{ error }
153+
);
154+
}
165155
});
166156
}
167-
}, [PENDO_URL, accountId, hasConsentEnabled, visitorId]);
157+
}, [accountId, visitorId]);
168158
};

packages/manager/src/utilities/analytics/utils.test.ts

Lines changed: 0 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,11 @@
11
import { generateTimeOfDay } from './customEventAnalytics';
22
import {
3-
checkOptanonConsent,
4-
getCookie,
53
getFormattedStringFromFormEventOptions,
6-
ONE_TRUST_COOKIE_CATEGORIES,
74
waitForAdobeAnalyticsToBeLoaded,
85
} from './utils';
96

107
import type { FormEventOptions } from './types';
118

12-
describe('getCookie', () => {
13-
beforeAll(() => {
14-
const mockCookies =
15-
'mycookie=my-cookie-value; OptanonConsent=cookie-consent-here; mythirdcookie=my-third-cookie;';
16-
vi.spyOn(document, 'cookie', 'get').mockReturnValue(mockCookies);
17-
});
18-
19-
it('should return the value of a cookie from document.cookie given its name, given cookie in middle position', () => {
20-
expect(getCookie('OptanonConsent')).toEqual('cookie-consent-here');
21-
});
22-
23-
it('should return the value of a cookie from document.cookie given its name, given cookie in first position', () => {
24-
expect(getCookie('mycookie')).toEqual('my-cookie-value');
25-
});
26-
27-
it('should return the value of a cookie from document.cookie given its name, given cookie in last position', () => {
28-
expect(getCookie('mythirdcookie')).toEqual('my-third-cookie');
29-
});
30-
31-
it('should return undefined if the cookie does not exist in document.cookie', () => {
32-
expect(getCookie('mysecondcookie')).toEqual(undefined);
33-
});
34-
});
35-
36-
describe('checkOptanonConsent', () => {
37-
it('should return true if consent is enabled for the given Optanon cookie category', () => {
38-
const mockPerformanceCookieConsentEnabled =
39-
'somestuffhere&groups=C0001%3A1%2CC0002%3A1%2CC0003%3A1%2CC0004%3A1%2CC0005%3A1&intType=6';
40-
41-
expect(
42-
checkOptanonConsent(
43-
mockPerformanceCookieConsentEnabled,
44-
ONE_TRUST_COOKIE_CATEGORIES['Performance Cookies']
45-
)
46-
).toEqual(true);
47-
});
48-
49-
it('should return false if consent is disabled for the given Optanon cookie category', () => {
50-
const mockPerformanceCookieConsentDisabled =
51-
'somestuffhere&groups=C0001%3A1%2CC0002%3A0%2CC0003%3A1%2CC0004%3A1%2CC0005%3A1&intType=6';
52-
53-
expect(
54-
checkOptanonConsent(
55-
mockPerformanceCookieConsentDisabled,
56-
ONE_TRUST_COOKIE_CATEGORIES['Performance Cookies']
57-
)
58-
).toEqual(false);
59-
});
60-
61-
it('should return false if the consent category does not exist in the cookie', () => {
62-
const mockNoPerformanceCookieCategory =
63-
'somestuffhere&groups=C0001%3A1%2CC0003%3A1%2CC0004%3A1%2CC0005%3A1&intType=6';
64-
65-
expect(
66-
checkOptanonConsent(
67-
mockNoPerformanceCookieCategory,
68-
ONE_TRUST_COOKIE_CATEGORIES['Performance Cookies']
69-
)
70-
).toEqual(false);
71-
});
72-
73-
it('should return false if the cookie is undefined', () => {
74-
expect(
75-
checkOptanonConsent(
76-
undefined,
77-
ONE_TRUST_COOKIE_CATEGORIES['Performance Cookies']
78-
)
79-
).toEqual(false);
80-
});
81-
});
82-
839
describe('generateTimeOfDay', () => {
8410
it('should generate human-readable time of day', () => {
8511
expect(generateTimeOfDay(0)).toBe('Early Morning');

packages/manager/src/utilities/analytics/utils.ts

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,60 +11,6 @@ import type {
1111
FormStepEvent,
1212
} from './types';
1313

14-
/**
15-
* Based on Login's OneTrust cookie list
16-
*/
17-
export const ONE_TRUST_COOKIE_CATEGORIES = {
18-
'Functional Cookies': 'C0003',
19-
'Performance Cookies': 'C0002', // Analytics cookies fall into this category
20-
'Social Media Cookies': 'C0004',
21-
'Strictly Necessary Cookies': 'C0001',
22-
'Targeting Cookies': 'C0005',
23-
} as const;
24-
25-
/**
26-
* Given the name of a cookie, parses the document.cookie string and returns the cookie's value.
27-
* @param name cookie's name
28-
* @returns value of cookie if it exists in the document; else, undefined
29-
*/
30-
export const getCookie = (name: string) => {
31-
const cookies = document.cookie.split(';');
32-
33-
const selectedCookie = cookies.find(
34-
(cookie) => cookie.trim().startsWith(name + '=') // Trim whitespace so position in cookie string doesn't matter
35-
);
36-
37-
return selectedCookie?.trim().substring(name.length + 1);
38-
};
39-
40-
/**
41-
* This function parses the categories in the OptanonConsent cookie to check if consent is provided.
42-
* @param optanonCookie the OptanonConsent cookie from OneTrust
43-
* @param selectedCategory the category code based on cookie type
44-
* @returns true if the user has consented to cookie enablement for the category; else, false
45-
*/
46-
export const checkOptanonConsent = (
47-
optanonCookie: string | undefined,
48-
selectedCategory: (typeof ONE_TRUST_COOKIE_CATEGORIES)[keyof typeof ONE_TRUST_COOKIE_CATEGORIES]
49-
): boolean => {
50-
const optanonGroups = optanonCookie?.match(/groups=([^&]*)/);
51-
52-
if (!optanonCookie || !optanonGroups) {
53-
return false;
54-
}
55-
56-
// Optanon consent groups will be of the form: "C000[N]:[0/1]".
57-
const unencodedOptanonGroups = decodeURIComponent(optanonGroups[1]).split(
58-
','
59-
);
60-
return unencodedOptanonGroups.some((consentGroup) => {
61-
if (consentGroup.includes(selectedCategory)) {
62-
return Number(consentGroup.split(':')[1]) === 1; // Cookie enabled
63-
}
64-
return false;
65-
});
66-
};
67-
6814
/**
6915
* Sends a direct call rule events to Adobe for a Component Click (and optionally, with `data`, Component Details).
7016
* This should be used for all custom events other than form events, which should use sendFormEvent.

packages/utilities/src/hooks/useScript.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ export const loadScript = (
6060
}
6161
} else {
6262
// Grab existing script status from attribute and set to state.
63-
options?.setStatus?.(script.getAttribute('data-status') as ScriptStatus);
63+
const existingStatus = script.getAttribute('data-status') as ScriptStatus;
64+
options?.setStatus?.(existingStatus);
65+
resolve({ status: existingStatus });
6466
}
6567
// Script event handler to update status in state
6668
// Note: Even if the script already exists we still need to add

0 commit comments

Comments
 (0)