Skip to content

Commit

Permalink
change: [M3-8390] - Initialize Pendo on Cloud Manager (#10982)
Browse files Browse the repository at this point in the history
* Save work

* Adapt install script and get usePendo hook working

* Clean up logging

* Add sha256 hashing for ids

* Clean up for readability

* Sanitize the URLs

* Add env var to .env.example

* Added changeset: Add Pendo to Cloud Manager

* Make IDs unique in multiple environments

* Address feedback: add auth matching to regex

* Address feedback: reference link in code comment for context
  • Loading branch information
mjac0bs authored Oct 7, 2024
1 parent 5155c6a commit 7917700
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tech Stories
---

Add Pendo to Cloud Manager ([#10982](https://github.com/linode/manager/pull/10982))
3 changes: 3 additions & 0 deletions packages/manager/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ REACT_APP_LKE_HIGH_AVAILABILITY_PRICE='60'
# Adobe Analytics:
# REACT_APP_ADOBE_ANALYTICS_URL=

# Pendo:
# REACT_APP_PENDO_API_KEY=

# Linode Docs search with Algolia:
# REACT_APP_ALGOLIA_APPLICATION_ID=
# REACT_APP_ALGOLIA_SEARCH_KEY=
Expand Down
1 change: 1 addition & 0 deletions packages/manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"highlight.js": "~10.4.1",
"immer": "^9.0.6",
"ipaddr.js": "^1.9.1",
"js-sha256": "^0.11.0",
"jspdf": "^2.5.2",
"jspdf-autotable": "^3.5.14",
"launchdarkly-react-client-sdk": "3.0.10",
Expand Down
2 changes: 2 additions & 0 deletions packages/manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { GoTo } from './GoTo';
import { useAdobeAnalytics } from './hooks/useAdobeAnalytics';
import { useInitialRequests } from './hooks/useInitialRequests';
import { useNewRelic } from './hooks/useNewRelic';
import { usePendo } from './hooks/usePendo';
import { MainContent } from './MainContent';
import { useEventsPoller } from './queries/events/events';
// import { Router } from './Router';
Expand Down Expand Up @@ -63,6 +64,7 @@ const BaseApp = withDocumentTitleProvider(
const GlobalListeners = () => {
useEventsPoller();
useAdobeAnalytics();
usePendo();
useNewRelic();
return null;
};
5 changes: 5 additions & 0 deletions packages/manager/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,15 @@ export const LONGVIEW_ROOT = 'https://longview.linode.com/fetch';
export const SENTRY_URL = import.meta.env.REACT_APP_SENTRY_URL;
export const LOGIN_SESSION_LIFETIME_MS = 45 * 60 * 1000;
export const OAUTH_TOKEN_REFRESH_TIMEOUT = LOGIN_SESSION_LIFETIME_MS / 2;

/** Adobe Analytics */
export const ADOBE_ANALYTICS_URL = import.meta.env
.REACT_APP_ADOBE_ANALYTICS_URL;
export const NUM_ADOBE_SCRIPTS = 3;

/** Pendo */
export const PENDO_API_KEY = import.meta.env.REACT_APP_PENDO_API_KEY;

/** for hard-coding token used for API Requests. Example: "Bearer 1234" */
export const ACCESS_TOKEN = import.meta.env.REACT_APP_ACCESS_TOKEN;

Expand Down
1 change: 1 addition & 0 deletions packages/manager/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface ImportMetaEnv {
REACT_APP_MOCK_SERVICE_WORKER?: string;
REACT_APP_PAYPAL_CLIENT_ID?: string;
REACT_APP_PAYPAL_ENV?: string;
REACT_APP_PENDO_API_KEY?: string;
// TODO: Parent/Child - Remove once we're off mocks.
REACT_APP_PROXY_PAT?: string;
REACT_APP_SENTRY_URL?: string;
Expand Down
137 changes: 137 additions & 0 deletions packages/manager/src/hooks/usePendo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { sha256 } from 'js-sha256';
import React from 'react';

import { APP_ROOT, PENDO_API_KEY } from 'src/constants';
import { useAccount } from 'src/queries/account/account.js';
import { useProfile } from 'src/queries/profile/profile';

import { loadScript } from './useScript';

declare global {
interface Window {
pendo: any;
}
}

/**
* This function prevents address ID collisions leading to muddled data between environments. Account and visitor IDs must be unique per API-key.
* See: https://support.pendo.io/hc/en-us/articles/360031862352-Pendo-in-multiple-environments-for-development-and-testing
* @returns Unique SHA256 hash of ID and the environment; else, undefined if missing values to hash.
*/
const hashUniquePendoId = (id: string | undefined) => {
const pendoEnv =
APP_ROOT === 'https://cloud.linode.com' ? 'production' : 'non-production';

if (!id || !APP_ROOT) {
return;
}

return sha256(id + pendoEnv);
};

/**
* Initializes our Pendo analytics script on mount.
*/
export const usePendo = () => {
const { data: account } = useAccount();
const { data: profile } = useProfile();

const accountId = hashUniquePendoId(account?.euuid);
const visitorId = hashUniquePendoId(profile?.uid.toString());

const PENDO_URL = `https://cdn.pendo.io/agent/static/${PENDO_API_KEY}/pendo.js`;

React.useEffect(() => {
// Adapted Pendo install script for readability
// Refer to: https://support.pendo.io/hc/en-us/articles/21362607464987-Components-of-the-install-script#01H6S2EXET8C9FGSHP08XZAE4F

// Set up Pendo namespace and queue
const pendo = (window['pendo'] = window['pendo'] || {});
pendo._q = pendo._q || [];

// Define the methods Pendo uses in a queue
const methodNames = [
'initialize',
'identify',
'updateOptions',
'pageLoad',
'track',
];

// Enqueue methods and their arguments on the Pendo object
methodNames.forEach((_, index) => {
(function (method) {
pendo[method] =
pendo[method] ||
function () {
pendo._q[method === methodNames[0] ? 'unshift' : 'push'](
// eslint-disable-next-line prefer-rest-params
[method].concat([].slice.call(arguments, 0))
);
};
})(methodNames[index]);
});

// Load Pendo script into the head HTML tag, then initialize Pendo with metadata
loadScript(PENDO_URL, {
location: 'head',
}).then(() => {
window.pendo.initialize({
account: {
id: accountId, // Highly recommended, required if using Pendo Feedback
// name: // Optional
// is_paying: // Recommended if using Pendo Feedback
// monthly_value:// Recommended if using Pendo Feedback
// planLevel: // Optional
// planPrice: // Optional
// creationDate: // Optional

// You can add any additional account level key-values here,
// as long as it's not one of the above reserved names.
},
// Controls what URLs we send to Pendo. Refer to: https://agent.pendo.io/advanced/location/.
location: {
transforms: [
{
action: 'Clear',
attr: 'hash',
},
{
action: 'Clear',
attr: 'search',
},
{
action: 'Replace',
attr: 'pathname',
data(url: string) {
const idMatchingRegex = /\d+$/;
const userPathMatchingRegex = /(users\/).*/;
const oauthPathMatchingRegex = /oauth\/callback#access_token/;
if (
idMatchingRegex.test(url) ||
oauthPathMatchingRegex.test(url)
) {
// Removes everything after the last /
return url.replace(/\/[^\/]*$/, '/');
} else if (userPathMatchingRegex.test(url)) {
// Removes everything after /users
return url.replace(userPathMatchingRegex, '$1');
}
return url;
},
},
],
},
visitor: {
id: visitorId, // Required if user is logged in
// email: // Recommended if using Pendo Feedback, or NPS Email
// full_name: // Recommended if using Pendo Feedback
// role: // Optional

// You can add any additional visitor level key-values here,
// as long as it's not one of the above reserved names.
},
});
});
}, [PENDO_URL, accountId, visitorId]);
};
2 changes: 1 addition & 1 deletion packages/manager/src/hooks/useScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const loadScript = (
return resolve({ status: 'idle' });
}
// Fetch existing script element by src
// It may have been added by another intance of this hook
// It may have been added by another instance of this hook
let script = document.querySelector(
`script[src='${src}']`
) as HTMLScriptElement;
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6537,6 +6537,11 @@ joycon@^3.1.1:
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==

js-sha256@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.11.0.tgz#256a921d9292f7fe98905face82e367abaca9576"
integrity sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==

"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
Expand Down

0 comments on commit 7917700

Please sign in to comment.