@codecademy/tracking 1.0.32-alpha.dcd01566d8.0 → 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -80,20 +80,15 @@ Integrations are loaded in an intentionally layered manner for CCPA/GDPR complia
80
80
 
81
81
  1. Wait 1000ms to allow any other post-hydration logic to run first
82
82
  2. Load in OneTrust's banner and wait for its `OptanonWrapper` callback
83
- 3. [Segment's copy-and-paste snippet](https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/quickstart/#step-2-copy-the-segment-snippet) is run to load the Segment global library
84
- 4. Destination integrations for Segment are fetched
85
- 5. Those integrations are compared against the user's consent decisions into a list of allowed destinations
86
- 6. We load only those allowed destinations using Segment's `analytics.load`
83
+ 3. Load GTM
87
84
 
88
85
  ```ts
89
86
  import { initializeTrackingIntegrations } from '@codecademy/tracking';
90
87
 
91
88
  setTimeout(() => {
92
89
  initializeTrackingIntegrations({
93
- onError: logError,
94
- production: true,
90
+ environment: process.env.NODE_ENV,
95
91
  scope: window,
96
- writeKey: 'my-segment-write-key',
97
92
  });
98
93
  }, 1000);
99
94
  ```
@@ -0,0 +1,8 @@
1
+ import { TrackingWindow } from './types';
2
+ export type GTMSettings = {
3
+ environment: string;
4
+ scope: TrackingWindow;
5
+ optedOutExternalTracking?: boolean;
6
+ };
7
+ export declare const OPT_OUT_DATALAYER_VAR = "user_opted_out_external_tracking";
8
+ export declare const initializeGTM: ({ scope, environment, optedOutExternalTracking, }: GTMSettings) => void;
@@ -0,0 +1,27 @@
1
+ export const OPT_OUT_DATALAYER_VAR = 'user_opted_out_external_tracking';
2
+ export const initializeGTM = _ref => {
3
+ var _scope$dataLayer;
4
+ let scope = _ref.scope,
5
+ environment = _ref.environment,
6
+ optedOutExternalTracking = _ref.optedOutExternalTracking;
7
+ (_scope$dataLayer = scope.dataLayer) !== null && _scope$dataLayer !== void 0 ? _scope$dataLayer : scope.dataLayer = [];
8
+ scope.dataLayer.push({
9
+ 'gtm.start': new Date().getTime(),
10
+ event: 'gtm.js'
11
+ });
12
+ if (optedOutExternalTracking) {
13
+ scope.dataLayer.push({
14
+ [OPT_OUT_DATALAYER_VAR]: true
15
+ });
16
+ }
17
+ let preview_env = '';
18
+ if (environment === 'development') {
19
+ preview_env = '&gtm_auth=DoN0WSxjuUkImaph8PYXmA&gtm_preview=env-233';
20
+ } else if (environment === 'staging') {
21
+ preview_env = '&gtm_auth=VrQuCDuWXkLlTwNHJYEKTg&gtm_preview=env-232';
22
+ }
23
+ const gtm = document.createElement('script');
24
+ gtm.async = true;
25
+ gtm.src = "https://www.googletagmanager.com/gtm.js?id=GTM-KTLK85W".concat(preview_env);
26
+ document.getElementsByTagName('head')[0].appendChild(gtm);
27
+ };
@@ -1,13 +1,9 @@
1
1
  import { TrackingWindow } from './types';
2
2
  export type TrackingIntegrationsSettings = {
3
3
  /**
4
- * Called whenever a network request fails.
4
+ * Current environment.
5
5
  */
6
- onError: (message: string) => void;
7
- /**
8
- * Whether this is running in a production environment.
9
- */
10
- production: boolean;
6
+ environment: string;
11
7
  /**
12
8
  * Global scope (often the window) where globals such as analytics are stored.
13
9
  */
@@ -16,12 +12,8 @@ export type TrackingIntegrationsSettings = {
16
12
  * Whether user has opted out or is excluded from external tracking
17
13
  */
18
14
  optedOutExternalTracking?: boolean;
19
- /**
20
- * Segment write key.
21
- */
22
- writeKey: string;
23
15
  };
24
16
  /**
25
17
  * @see README.md for details and usage.
26
18
  */
27
- export declare const initializeTrackingIntegrations: ({ onError, production, scope, optedOutExternalTracking, writeKey, }: TrackingIntegrationsSettings) => Promise<void>;
19
+ export declare const initializeTrackingIntegrations: ({ environment, scope, optedOutExternalTracking, }: TrackingIntegrationsSettings) => Promise<void>;
@@ -1,54 +1,25 @@
1
- import { conditionallyLoadAnalytics } from './conditionallyLoadAnalytics';
2
- import { fetchDestinationsForWriteKey } from './fetchDestinationsForWriteKey';
3
- import { getConsentDecision } from './getConsentDecision';
4
- import { mapDestinations } from './mapDestinations';
1
+ import { initializeGTM } from './gtm';
5
2
  import { initializeOneTrust } from './onetrust';
6
- import { runSegmentSnippet } from './runSegmentSnippet';
7
3
  /**
8
4
  * @see README.md for details and usage.
9
5
  */
10
6
  export const initializeTrackingIntegrations = async _ref => {
11
- let onError = _ref.onError,
12
- production = _ref.production,
7
+ let environment = _ref.environment,
13
8
  scope = _ref.scope,
14
- optedOutExternalTracking = _ref.optedOutExternalTracking,
15
- writeKey = _ref.writeKey;
9
+ optedOutExternalTracking = _ref.optedOutExternalTracking;
16
10
  // 1. Wait 1000ms to allow any other post-hydration logic to run first
17
11
  await new Promise(resolve => setTimeout(resolve, 1000));
18
12
 
19
13
  // 2. Load in OneTrust's banner and wait for its `OptanonWrapper` callback
20
14
  await initializeOneTrust({
21
15
  scope,
22
- production
16
+ environment
23
17
  });
24
18
 
25
- // 3. Segment's copy-and-paste snippet is run to load the Segment global library
26
- runSegmentSnippet();
27
-
28
- // 4. Destination integrations for Segment are fetched
29
- const destinations = await fetchDestinationsForWriteKey({
30
- onError,
31
- writeKey
32
- });
33
- if (!destinations) {
34
- return;
35
- }
36
- const consentDecision = getConsentDecision({
19
+ // 3. Load GTM
20
+ initializeGTM({
37
21
  scope,
22
+ environment,
38
23
  optedOutExternalTracking
39
24
  });
40
-
41
- // 5. Those integrations are compared against the user's consent decisions into a list of allowed destinations
42
- const _mapDestinations = mapDestinations({
43
- consentDecision,
44
- destinations
45
- }),
46
- destinationPreferences = _mapDestinations.destinationPreferences;
47
-
48
- // 6. We load only those allowed destinations using Segment's `analytics.load`
49
- conditionallyLoadAnalytics({
50
- analytics: scope.analytics,
51
- destinationPreferences,
52
- writeKey
53
- });
54
25
  };
@@ -1,6 +1,6 @@
1
1
  import { TrackingWindow } from './types';
2
2
  export type OneTrustSettings = {
3
- production: boolean;
3
+ environment: string;
4
4
  scope: TrackingWindow;
5
5
  };
6
- export declare const initializeOneTrust: ({ production, scope, }: OneTrustSettings) => Promise<void>;
6
+ export declare const initializeOneTrust: ({ environment, scope, }: OneTrustSettings) => Promise<void>;
@@ -1,11 +1,11 @@
1
1
  export const initializeOneTrust = async _ref => {
2
- let production = _ref.production,
2
+ let environment = _ref.environment,
3
3
  scope = _ref.scope;
4
4
  const script = document.createElement('script');
5
5
  script.setAttribute('async', 'true');
6
6
  script.setAttribute('src', 'https://cdn.cookielaw.org/scripttemplates/otSDKStub.js');
7
7
  script.setAttribute('type', 'text/javascript');
8
- script.setAttribute('data-domain-script', "cfa7b129-f37b-4f5a-9991-3f75ba7b85fb".concat(production ? '' : '-test'));
8
+ script.setAttribute('data-domain-script', "cfa7b129-f37b-4f5a-9991-3f75ba7b85fb".concat(environment === 'production' ? '' : '-test'));
9
9
  document.body.appendChild(script);
10
10
  const style = document.createElement('style');
11
11
  style.textContent = rawStyles;
@@ -1,18 +1,5 @@
1
1
  import { Consent } from './consent';
2
- export interface SegmentAnalytics {
3
- initialize?: boolean;
4
- load(writeKey: string, options: SegmentAnalyticsOptions): void;
5
- page(): void;
6
- }
7
- export interface SegmentDestination {
8
- category: string;
9
- id: string;
10
- }
11
- export interface SegmentAnalyticsOptions {
12
- integrations: Record<string, boolean>;
13
- }
14
2
  export interface TrackingWindow {
15
- analytics?: SegmentAnalytics;
16
3
  dataLayer?: unknown[];
17
4
  OnetrustActiveGroups?: Consent[] | string;
18
5
  OptanonWrapper?: () => void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@codecademy/tracking",
3
3
  "description": "Tracking library for Codecademy",
4
- "version": "1.0.32-alpha.dcd01566d8.0",
4
+ "version": "1.0.32",
5
5
  "author": "Codecademy Engineering <dev@codecademy.com>",
6
6
  "files": [
7
7
  "dist/**"
@@ -13,5 +13,5 @@
13
13
  "access": "public"
14
14
  },
15
15
  "repository": "git@github.com:codecademy-engineering/mono.git",
16
- "gitHead": "f7712ac5159433c4d3c0a5459cb464a8b224e802"
16
+ "gitHead": "389b25c0b3df30f006b82c5151799d1747ea6778"
17
17
  }
@@ -1,7 +0,0 @@
1
- import { SegmentAnalytics } from './types';
2
- export type AnalyticsLoadOptions = {
3
- analytics: SegmentAnalytics;
4
- destinationPreferences: Record<string, boolean>;
5
- writeKey: string;
6
- };
7
- export declare const conditionallyLoadAnalytics: ({ analytics, destinationPreferences, writeKey, }: AnalyticsLoadOptions) => void;
@@ -1,11 +0,0 @@
1
- export const conditionallyLoadAnalytics = _ref => {
2
- let analytics = _ref.analytics,
3
- destinationPreferences = _ref.destinationPreferences,
4
- writeKey = _ref.writeKey;
5
- if (analytics.initialize) {
6
- return;
7
- }
8
- analytics.load(writeKey, {
9
- integrations: destinationPreferences
10
- });
11
- };
@@ -1,6 +0,0 @@
1
- import { SegmentDestination } from './types';
2
- export type FetchDestinationsSettings = {
3
- onError: (message: string) => void;
4
- writeKey: string;
5
- };
6
- export declare const fetchDestinationsForWriteKey: ({ writeKey, onError, }: FetchDestinationsSettings) => Promise<SegmentDestination[] | undefined>;
@@ -1,26 +0,0 @@
1
- const knownFetchFailures = ['Failed to fetch', 'Load failed', 'NetworkError when attempting to fetch resource', 'Resource blocked by content blocker'];
2
- export const fetchDestinationsForWriteKey = async _ref => {
3
- let writeKey = _ref.writeKey,
4
- onError = _ref.onError;
5
- const filteredOnError = error => {
6
- if (!knownFetchFailures.some(failure => error.includes(failure))) {
7
- onError(error);
8
- }
9
- };
10
- try {
11
- const response = await fetch("https://cdn.segment.com/v1/projects/".concat(writeKey, "/integrations"));
12
- if (!response.ok) {
13
- filteredOnError("Failed to fetch integrations for write key ".concat(writeKey, ": HTTP ").concat(response.status, " ").concat(response.statusText));
14
- return [];
15
- }
16
- const destinations = await response.json();
17
- for (const destination of destinations) {
18
- destination.id = destination.creationName;
19
- delete destination.creationName;
20
- }
21
- return destinations;
22
- } catch (error) {
23
- filteredOnError("Unknown error fetching Segment destinations for write key ".concat(writeKey, ": ").concat(error));
24
- return [];
25
- }
26
- };
@@ -1,8 +0,0 @@
1
- import { Consent } from './consent';
2
- import { TrackingWindow } from './types';
3
- export interface ConsentDecisionOptions {
4
- scope: TrackingWindow;
5
- optedOutExternalTracking?: boolean;
6
- }
7
- export declare const OPT_OUT_DATALAYER_VAR = "user_opted_out_external_tracking";
8
- export declare const getConsentDecision: ({ scope, optedOutExternalTracking, }: ConsentDecisionOptions) => Consent[];
@@ -1,27 +0,0 @@
1
- import { Consent } from './consent';
2
- export const OPT_OUT_DATALAYER_VAR = 'user_opted_out_external_tracking';
3
- export const getConsentDecision = _ref => {
4
- let scope = _ref.scope,
5
- optedOutExternalTracking = _ref.optedOutExternalTracking;
6
- let consentDecision = [];
7
- if (typeof scope.OnetrustActiveGroups === 'string') {
8
- consentDecision = scope.OnetrustActiveGroups.split(',').filter(Boolean);
9
- } else if (scope.OnetrustActiveGroups) {
10
- consentDecision = scope.OnetrustActiveGroups;
11
- }
12
- if (optedOutExternalTracking) {
13
- var _scope$dataLayer;
14
- /**
15
- * If user has already opted out of everything but the essentials
16
- * don't force them to consent to Functional & Performance trackers
17
- */
18
- if (consentDecision.length > 2) {
19
- consentDecision = [Consent.StrictlyNecessary, Consent.Functional, Consent.Performance];
20
- }
21
- (_scope$dataLayer = scope.dataLayer) !== null && _scope$dataLayer !== void 0 ? _scope$dataLayer : scope.dataLayer = [];
22
- scope.dataLayer.push({
23
- [OPT_OUT_DATALAYER_VAR]: true
24
- });
25
- }
26
- return consentDecision;
27
- };
@@ -1,12 +0,0 @@
1
- import { Consent } from './consent';
2
- import { SegmentDestination } from './types';
3
- export type DestinationMapOptions = {
4
- consentDecision?: Consent[];
5
- destinations: SegmentDestination[];
6
- };
7
- /**
8
- * @see https://www.notion.so/codecademy/GDPR-Compliance-141ebcc7ffa542daa0da56e35f482b41
9
- */
10
- export declare const mapDestinations: ({ consentDecision, destinations, }: DestinationMapOptions) => {
11
- destinationPreferences: Record<string, boolean>;
12
- };
@@ -1,39 +0,0 @@
1
- import { Consent } from './consent';
2
- // The Functional category may need to be added here in the future.
3
- const targetingCategories = ['Advertising', 'Attribution', 'Email Marketing'];
4
- const performanceCategories = ['Analytics', 'Customer Success', 'Surveys', 'Heatmaps & Recording'];
5
- const functionalCategories = ['SMS & Push Notifications'];
6
-
7
- /**
8
- * @see https://www.notion.so/codecademy/GDPR-Compliance-141ebcc7ffa542daa0da56e35f482b41
9
- */
10
- export const mapDestinations = _ref => {
11
- let _ref$consentDecision = _ref.consentDecision,
12
- consentDecision = _ref$consentDecision === void 0 ? [Consent.StrictlyNecessary] : _ref$consentDecision,
13
- destinations = _ref.destinations;
14
- const destinationPreferences = Object.assign({
15
- 'Segment.io': consentDecision.includes(Consent.Functional)
16
- }, ...destinations.map(dest => {
17
- if (targetingCategories.includes(dest.category)) {
18
- return {
19
- [dest.id]: consentDecision.includes(Consent.Targeting)
20
- };
21
- }
22
- if (performanceCategories.includes(dest.category)) {
23
- return {
24
- [dest.id]: consentDecision.includes(Consent.Performance)
25
- };
26
- }
27
- if (functionalCategories.includes(dest.category)) {
28
- return {
29
- [dest.id]: consentDecision.includes(Consent.Functional)
30
- };
31
- }
32
- return {
33
- [dest.id]: true
34
- };
35
- }));
36
- return {
37
- destinationPreferences
38
- };
39
- };
@@ -1,7 +0,0 @@
1
- /**
2
- * This code is copypasta from the Segment documentation.
3
- * It creates the global analytics object and loads the Segment Analytics API that uses it.
4
- *
5
- * @see https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/quickstart/#step-2-copy-the-segment-snippet
6
- */
7
- export declare const runSegmentSnippet: () => void;
@@ -1,69 +0,0 @@
1
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2
- // @ts-nocheck
3
-
4
- /**
5
- * This code is copypasta from the Segment documentation.
6
- * It creates the global analytics object and loads the Segment Analytics API that uses it.
7
- *
8
- * @see https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/quickstart/#step-2-copy-the-segment-snippet
9
- */
10
- export const runSegmentSnippet = () => {
11
- var _window;
12
- // Create a queue, but don't obliterate an existing one!
13
- (_window = window).analytics || (_window.analytics = []);
14
- const _window2 = window,
15
- analytics = _window2.analytics;
16
-
17
- // If the real analytics.js is already on the page return.
18
- if (analytics.initialize) return;
19
-
20
- // If the snippet was invoked already show an error.
21
- if (analytics.invoked) {
22
- console.error('Segment snippet included twice.');
23
- return;
24
- }
25
-
26
- // Invoked flag, to make sure the snippet
27
- // is never invoked twice.
28
- analytics.invoked = true;
29
-
30
- // A list of the methods in Analytics.js to stub.
31
- analytics.methods = ['trackSubmit', 'trackClick', 'trackLink', 'trackForm', 'pageview', 'identify', 'reset', 'group', 'track', 'ready', 'alias', 'debug', 'page', 'once', 'off', 'on', 'addSourceMiddleware', 'addIntegrationMiddleware', 'setAnonymousId', 'addDestinationMiddleware'];
32
-
33
- // Define a factory to create stubs. These are placeholders
34
- // for methods in Analytics.js so that you never have to wait
35
- // for it to load to actually record data. The `method` is
36
- // stored as the first argument, so we can replay the data.
37
- analytics.factory = function (method) {
38
- return function () {
39
- const args = Array.prototype.slice.call(arguments);
40
- args.unshift(method);
41
- analytics.push(args);
42
- return analytics;
43
- };
44
- };
45
-
46
- // For each of our methods, generate a queueing stub.
47
- for (let i = 0; i < analytics.methods.length; i += 1) {
48
- const key = analytics.methods[i];
49
- analytics[key] = analytics.factory(key);
50
- }
51
-
52
- // Define a method to load Analytics.js from our CDN,
53
- // and that will be sure to only ever load it once.
54
- analytics.load = function (key, options) {
55
- // Create an async script element based on your key.
56
- const script = document.createElement('script');
57
- script.type = 'text/javascript';
58
- script.async = true;
59
- script.src = 'https://cdn.segment.com/analytics.js/v1/' + key + '/analytics.min.js';
60
-
61
- // Insert our script next to the first script element.
62
- const first = document.getElementsByTagName('script')[0];
63
- first.parentNode.insertBefore(script, first);
64
- analytics._loadOptions = options;
65
- };
66
-
67
- // Add a version to keep track of what's in the wild.
68
- analytics.SNIPPET_VERSION = '4.1.0';
69
- };