@codecademy/tracking 0.25.1-alpha.42681.0 → 0.25.1-alpha.5b36ce.0
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/.turbo/turbo-build.log +7 -2
- package/CHANGELOG.md +1 -1
- package/dist/events/__tests__/track.test.js +12 -5
- package/dist/events/__tests__/user.test.js +5 -0
- package/dist/events/track.js +30 -11
- package/dist/events/types.js +0 -1
- package/dist/events/user.js +6 -0
- package/dist/integrations/conditionallyLoadAnalytics.js +7 -4
- package/dist/integrations/consent.js +1 -0
- package/dist/integrations/device.js +5 -5
- package/dist/integrations/fetchDestinationsForWriteKey.js +19 -0
- package/dist/integrations/index.js +33 -13
- package/dist/integrations/mapDestinations.js +13 -3
- package/dist/integrations/onetrust.js +8 -3
- package/dist/integrations/types.d.ts +1 -0
- package/dist/integrations/types.js +0 -1
- package/package.json +3 -3
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
[35m@codecademy/tracking:build: [0mcache hit, replaying output [
|
|
1
|
+
[35m@codecademy/tracking:build: [0mcache hit, replaying output [2m53b2821e10cf2062[0m
|
|
2
2
|
[35m@codecademy/tracking:build: [0m$ yarn build:clean && yarn build:compile && yarn build:types
|
|
3
3
|
[35m@codecademy/tracking:build: [0m$ rm -rf dist
|
|
4
4
|
[35m@codecademy/tracking:build: [0m$ babel ./src --out-dir ./dist --copy-files --extensions ".ts,.tsx"
|
|
5
|
-
[35m@codecademy/tracking:build: [
|
|
5
|
+
[35m@codecademy/tracking:build: [0mBrowserslist: caniuse-lite is outdated. Please run:
|
|
6
|
+
[35m@codecademy/tracking:build: [0mnpx browserslist@latest --update-db
|
|
7
|
+
[35m@codecademy/tracking:build: [0m
|
|
8
|
+
[35m@codecademy/tracking:build: [0mWhy you should do it regularly:
|
|
9
|
+
[35m@codecademy/tracking:build: [0mhttps://github.com/browserslist/browserslist#browsers-data-updating
|
|
10
|
+
[35m@codecademy/tracking:build: [0mSuccessfully compiled 18 files with Babel (2060ms).
|
|
6
11
|
[35m@codecademy/tracking:build: [0m$ tsc --emitDeclarationOnly
|
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
-
### [0.25.1-alpha.
|
|
6
|
+
### [0.25.1-alpha.5b36ce.0](https://github.com/Codecademy/client-modules/compare/@codecademy/tracking@0.25.0...@codecademy/tracking@0.25.1-alpha.5b36ce.0) (2022-10-24)
|
|
7
7
|
|
|
8
8
|
**Note:** Version bump only for package @codecademy/tracking
|
|
9
9
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
var _class;
|
|
2
|
-
|
|
3
|
-
function
|
|
1
|
+
var _class, _temp;
|
|
2
|
+
|
|
3
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
|
|
4
|
+
|
|
5
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
|
|
6
|
+
|
|
4
7
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
8
|
+
|
|
5
9
|
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
10
|
+
|
|
6
11
|
import { createTracker } from '../track';
|
|
7
12
|
var apiBaseUrl = 'https://www.codecademy.com';
|
|
8
13
|
var fakeWindow = {
|
|
@@ -24,9 +29,9 @@ Object.defineProperties(window, {
|
|
|
24
29
|
value: fakeWindow.location
|
|
25
30
|
},
|
|
26
31
|
Request: {
|
|
27
|
-
value: (_class = function MockRequest() {
|
|
32
|
+
value: (_temp = _class = function MockRequest() {
|
|
28
33
|
_classCallCheck(this, MockRequest);
|
|
29
|
-
}, _defineProperty(_class, "keepalive", true),
|
|
34
|
+
}, _defineProperty(_class, "keepalive", true), _temp)
|
|
30
35
|
}
|
|
31
36
|
});
|
|
32
37
|
Object.defineProperty(window.document, 'title', {
|
|
@@ -41,6 +46,7 @@ jest.mock('../../integrations/device', function () {
|
|
|
41
46
|
get getClientType() {
|
|
42
47
|
return mockClientType;
|
|
43
48
|
}
|
|
49
|
+
|
|
44
50
|
};
|
|
45
51
|
});
|
|
46
52
|
describe('createTracker', function () {
|
|
@@ -123,6 +129,7 @@ describe('createTracker', function () {
|
|
|
123
129
|
});
|
|
124
130
|
});
|
|
125
131
|
};
|
|
132
|
+
|
|
126
133
|
describeEvent('click');
|
|
127
134
|
describeEvent('impression');
|
|
128
135
|
describeEvent('visit');
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import _regeneratorRuntime from "@babel/runtime/regenerator";
|
|
2
|
+
|
|
2
3
|
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
|
|
4
|
+
|
|
3
5
|
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
|
|
6
|
+
|
|
4
7
|
import fetch from 'fetch-mock';
|
|
5
8
|
import { fetchUser } from '../user';
|
|
6
9
|
var apiBaseUrl = 'https://www.codecademy.com';
|
|
@@ -20,6 +23,7 @@ describe('fetchUser', function () {
|
|
|
20
23
|
fetch.getOnce('*', JSON.stringify(authUser));
|
|
21
24
|
_context.next = 3;
|
|
22
25
|
return fetchUser(apiBaseUrl);
|
|
26
|
+
|
|
23
27
|
case 3:
|
|
24
28
|
user = _context.sent;
|
|
25
29
|
expect(user).toEqual(authUser);
|
|
@@ -27,6 +31,7 @@ describe('fetchUser', function () {
|
|
|
27
31
|
expect(fetch.calls()[0][0]).toBe("".concat(apiBaseUrl, "/users/web"));
|
|
28
32
|
expect(fetch.calls()[0][1].method).toBe('GET');
|
|
29
33
|
expect(fetch.calls()[0][1].credentials).toBe('include');
|
|
34
|
+
|
|
30
35
|
case 9:
|
|
31
36
|
case "end":
|
|
32
37
|
return _context.stop();
|
package/dist/events/track.js
CHANGED
|
@@ -1,31 +1,46 @@
|
|
|
1
|
-
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly
|
|
2
|
-
|
|
1
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
|
|
2
|
+
|
|
3
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
|
|
4
|
+
|
|
3
5
|
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
6
|
+
|
|
4
7
|
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
|
|
8
|
+
|
|
5
9
|
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
10
|
+
|
|
6
11
|
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
12
|
+
|
|
7
13
|
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
|
|
8
|
-
|
|
14
|
+
|
|
15
|
+
function _iterableToArrayLimit(arr, i) { var _i = arr && (typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]); if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
|
|
16
|
+
|
|
9
17
|
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
18
|
+
|
|
10
19
|
import { getClientType } from '../integrations/device';
|
|
11
20
|
/* eslint-disable no-console */
|
|
12
21
|
|
|
13
22
|
var browserSupportsKeepalive = function browserSupportsKeepalive() {
|
|
14
23
|
return 'keepalive' in window.Request.prototype;
|
|
15
24
|
};
|
|
25
|
+
|
|
16
26
|
export var createTracker = function createTracker(_ref) {
|
|
17
27
|
var apiBaseUrl = _ref.apiBaseUrl,
|
|
18
|
-
|
|
28
|
+
verbose = _ref.verbose;
|
|
29
|
+
|
|
19
30
|
var beacon = function beacon(endpoint, data) {
|
|
20
31
|
var uri = new URL(endpoint, apiBaseUrl).toString();
|
|
21
32
|
var form = new FormData();
|
|
33
|
+
|
|
22
34
|
for (var _i2 = 0, _Object$entries = Object.entries(data); _i2 < _Object$entries.length; _i2++) {
|
|
23
35
|
var _ref4 = _Object$entries[_i2];
|
|
36
|
+
|
|
24
37
|
var _ref3 = _slicedToArray(_ref4, 2);
|
|
38
|
+
|
|
25
39
|
var k = _ref3[0];
|
|
26
40
|
var v = _ref3[1];
|
|
27
41
|
form.append(k, v.toString());
|
|
28
42
|
}
|
|
43
|
+
|
|
29
44
|
try {
|
|
30
45
|
// Firefox allows users to disable navigator.sendBeacon, and very old Safari versions don't have it.
|
|
31
46
|
// [WEB-1700]: Additionally, Chrome 79-80 gives "Illegal invocation" with ?., so through 2022 we should support them.
|
|
@@ -34,13 +49,12 @@ export var createTracker = function createTracker(_ref) {
|
|
|
34
49
|
if (navigator.sendBeacon && navigator.sendBeacon(uri, form)) {
|
|
35
50
|
return;
|
|
36
51
|
}
|
|
37
|
-
} catch (_unused) {
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Either way, we fall back to standard fetch if sendBeacon fails.
|
|
52
|
+
} catch (_unused) {// Even with the proper scoping, Chrome 79-80 still gives "Illegal invocation" crashes. Sigh.
|
|
53
|
+
} // Either way, we fall back to standard fetch if sendBeacon fails.
|
|
42
54
|
// We don't mind this rejecting with an error because it's tracking, and we'll know if that starts to fail.
|
|
43
55
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
56
|
+
|
|
57
|
+
|
|
44
58
|
window.fetch(uri, _objectSpread({
|
|
45
59
|
method: 'POST',
|
|
46
60
|
body: form
|
|
@@ -48,8 +62,10 @@ export var createTracker = function createTracker(_ref) {
|
|
|
48
62
|
keepalive: true
|
|
49
63
|
}));
|
|
50
64
|
};
|
|
65
|
+
|
|
51
66
|
var event = function event(category, _event, userData) {
|
|
52
67
|
var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
|
|
68
|
+
|
|
53
69
|
var properties = _objectSpread(_objectSpread({}, userData), {}, {
|
|
54
70
|
fullpath: window.location.pathname + window.location.search,
|
|
55
71
|
search: window.location.search,
|
|
@@ -59,6 +75,7 @@ export var createTracker = function createTracker(_ref) {
|
|
|
59
75
|
referrer: userData.referrer || window.document.referrer,
|
|
60
76
|
client: getClientType()
|
|
61
77
|
});
|
|
78
|
+
|
|
62
79
|
if (verbose) {
|
|
63
80
|
console.groupCollapsed("%cTracking Event Fired: ".concat(category, ":").concat(_event), 'color: #4b35ef; font-style: italic;');
|
|
64
81
|
console.log({
|
|
@@ -67,9 +84,9 @@ export var createTracker = function createTracker(_ref) {
|
|
|
67
84
|
properties: properties
|
|
68
85
|
});
|
|
69
86
|
console.groupEnd();
|
|
70
|
-
}
|
|
87
|
+
} // This allows the UTM query params to get registered in the user session.
|
|
88
|
+
|
|
71
89
|
|
|
72
|
-
// This allows the UTM query params to get registered in the user session.
|
|
73
90
|
var queryParams = window.location.search;
|
|
74
91
|
beacon("/analytics/".concat(category).concat(queryParams), {
|
|
75
92
|
category: category,
|
|
@@ -78,6 +95,7 @@ export var createTracker = function createTracker(_ref) {
|
|
|
78
95
|
gdpr_safe: "".concat(options.gdprSafe)
|
|
79
96
|
});
|
|
80
97
|
};
|
|
98
|
+
|
|
81
99
|
return {
|
|
82
100
|
event: event,
|
|
83
101
|
click: function click(data) {
|
|
@@ -91,6 +109,7 @@ export var createTracker = function createTracker(_ref) {
|
|
|
91
109
|
},
|
|
92
110
|
pushDataLayerEvent: function pushDataLayerEvent(eventName) {
|
|
93
111
|
var _ref5;
|
|
112
|
+
|
|
94
113
|
((_ref5 = window).dataLayer || (_ref5.dataLayer = [])).push({
|
|
95
114
|
event: eventName
|
|
96
115
|
});
|
package/dist/events/types.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/events/user.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import _regeneratorRuntime from "@babel/runtime/regenerator";
|
|
2
|
+
|
|
2
3
|
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
|
|
4
|
+
|
|
3
5
|
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
|
|
6
|
+
|
|
4
7
|
export var fetchUser = /*#__PURE__*/function () {
|
|
5
8
|
var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(apiBaseUrl) {
|
|
6
9
|
var response;
|
|
@@ -17,9 +20,11 @@ export var fetchUser = /*#__PURE__*/function () {
|
|
|
17
20
|
},
|
|
18
21
|
credentials: 'include'
|
|
19
22
|
});
|
|
23
|
+
|
|
20
24
|
case 2:
|
|
21
25
|
response = _context.sent;
|
|
22
26
|
return _context.abrupt("return", response.json());
|
|
27
|
+
|
|
23
28
|
case 4:
|
|
24
29
|
case "end":
|
|
25
30
|
return _context.stop();
|
|
@@ -27,6 +32,7 @@ export var fetchUser = /*#__PURE__*/function () {
|
|
|
27
32
|
}
|
|
28
33
|
}, _callee);
|
|
29
34
|
}));
|
|
35
|
+
|
|
30
36
|
return function fetchUser(_x) {
|
|
31
37
|
return _ref.apply(this, arguments);
|
|
32
38
|
};
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { getClientType } from './device';
|
|
2
2
|
export var conditionallyLoadAnalytics = function conditionallyLoadAnalytics(_ref) {
|
|
3
3
|
var analytics = _ref.analytics,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
destinationPreferences = _ref.destinationPreferences,
|
|
5
|
+
identifyPreferences = _ref.identifyPreferences,
|
|
6
|
+
user = _ref.user,
|
|
7
|
+
writeKey = _ref.writeKey;
|
|
8
|
+
|
|
8
9
|
if (analytics.initialize) {
|
|
9
10
|
return;
|
|
10
11
|
}
|
|
12
|
+
|
|
11
13
|
analytics.load(writeKey, {
|
|
12
14
|
integrations: destinationPreferences
|
|
13
15
|
});
|
|
14
16
|
analytics.page();
|
|
17
|
+
|
|
15
18
|
if (user) {
|
|
16
19
|
var identifyParams = {
|
|
17
20
|
email: user.email,
|
|
@@ -2,24 +2,24 @@
|
|
|
2
2
|
* @returns Whether the site is running both in ChromeOS and in PWA mode.
|
|
3
3
|
*/
|
|
4
4
|
export var isChromeOSPWA = function isChromeOSPWA() {
|
|
5
|
-
return isChromeOS() && 'getDigitalGoodsService' in window &&
|
|
6
|
-
// https://stackoverflow.com/questions/41742390/javascript-to-check-if-pwa-or-mobile-web
|
|
5
|
+
return isChromeOS() && 'getDigitalGoodsService' in window && // https://stackoverflow.com/questions/41742390/javascript-to-check-if-pwa-or-mobile-web
|
|
7
6
|
window.matchMedia('(display-mode: standalone)').matches;
|
|
8
7
|
};
|
|
9
|
-
|
|
10
8
|
/**
|
|
11
9
|
* @returns Whether the site is running in ChromeOS
|
|
12
10
|
*/
|
|
11
|
+
|
|
13
12
|
export var isChromeOS = function isChromeOS() {
|
|
14
|
-
return typeof navigator !== 'undefined' &&
|
|
15
|
-
// https://stackoverflow.com/questions/29657165/detecting-chrome-os-with-javascript
|
|
13
|
+
return typeof navigator !== 'undefined' && // https://stackoverflow.com/questions/29657165/detecting-chrome-os-with-javascript
|
|
16
14
|
/\bCrOS\b/.test(navigator.userAgent);
|
|
17
15
|
};
|
|
18
16
|
export var ClientTypes;
|
|
17
|
+
|
|
19
18
|
(function (ClientTypes) {
|
|
20
19
|
ClientTypes["PWA"] = "pwa";
|
|
21
20
|
ClientTypes["Default"] = "default";
|
|
22
21
|
})(ClientTypes || (ClientTypes = {}));
|
|
22
|
+
|
|
23
23
|
export var getClientType = function getClientType() {
|
|
24
24
|
return isChromeOSPWA() ? ClientTypes.PWA : ClientTypes.Default;
|
|
25
25
|
};
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import _regeneratorRuntime from "@babel/runtime/regenerator";
|
|
2
|
+
|
|
2
3
|
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
|
|
4
|
+
|
|
3
5
|
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
6
|
+
|
|
4
7
|
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
|
|
8
|
+
|
|
5
9
|
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
|
|
10
|
+
|
|
6
11
|
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
|
|
12
|
+
|
|
7
13
|
var knownFetchFailures = ['Failed to fetch', 'Load failed', 'NetworkError when attempting to fetch resource', 'Resource blocked by content blocker'];
|
|
8
14
|
export var fetchDestinationsForWriteKey = /*#__PURE__*/function () {
|
|
9
15
|
var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(_ref) {
|
|
10
16
|
var writeKey, onError, filteredOnError, response, destinations, _iterator, _step, destination;
|
|
17
|
+
|
|
11
18
|
return _regeneratorRuntime.wrap(function _callee$(_context) {
|
|
12
19
|
while (1) {
|
|
13
20
|
switch (_context.prev = _context.next) {
|
|
14
21
|
case 0:
|
|
15
22
|
writeKey = _ref.writeKey, onError = _ref.onError;
|
|
23
|
+
|
|
16
24
|
filteredOnError = function filteredOnError(error) {
|
|
17
25
|
if (!knownFetchFailures.some(function (failure) {
|
|
18
26
|
return error.includes(failure);
|
|
@@ -20,23 +28,30 @@ export var fetchDestinationsForWriteKey = /*#__PURE__*/function () {
|
|
|
20
28
|
onError(error);
|
|
21
29
|
}
|
|
22
30
|
};
|
|
31
|
+
|
|
23
32
|
_context.prev = 2;
|
|
24
33
|
_context.next = 5;
|
|
25
34
|
return fetch("https://cdn.segment.com/v1/projects/".concat(writeKey, "/integrations"));
|
|
35
|
+
|
|
26
36
|
case 5:
|
|
27
37
|
response = _context.sent;
|
|
38
|
+
|
|
28
39
|
if (response.ok) {
|
|
29
40
|
_context.next = 9;
|
|
30
41
|
break;
|
|
31
42
|
}
|
|
43
|
+
|
|
32
44
|
filteredOnError("Failed to fetch integrations for write key ".concat(writeKey, ": HTTP ").concat(response.status, " ").concat(response.statusText));
|
|
33
45
|
return _context.abrupt("return", []);
|
|
46
|
+
|
|
34
47
|
case 9:
|
|
35
48
|
_context.next = 11;
|
|
36
49
|
return response.json();
|
|
50
|
+
|
|
37
51
|
case 11:
|
|
38
52
|
destinations = _context.sent;
|
|
39
53
|
_iterator = _createForOfIteratorHelper(destinations);
|
|
54
|
+
|
|
40
55
|
try {
|
|
41
56
|
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
42
57
|
destination = _step.value;
|
|
@@ -48,12 +63,15 @@ export var fetchDestinationsForWriteKey = /*#__PURE__*/function () {
|
|
|
48
63
|
} finally {
|
|
49
64
|
_iterator.f();
|
|
50
65
|
}
|
|
66
|
+
|
|
51
67
|
return _context.abrupt("return", destinations);
|
|
68
|
+
|
|
52
69
|
case 17:
|
|
53
70
|
_context.prev = 17;
|
|
54
71
|
_context.t0 = _context["catch"](2);
|
|
55
72
|
filteredOnError("Unknown error fetching Segment destinations for write key ".concat(writeKey, ": ").concat(_context.t0));
|
|
56
73
|
return _context.abrupt("return", []);
|
|
74
|
+
|
|
57
75
|
case 21:
|
|
58
76
|
case "end":
|
|
59
77
|
return _context.stop();
|
|
@@ -61,6 +79,7 @@ export var fetchDestinationsForWriteKey = /*#__PURE__*/function () {
|
|
|
61
79
|
}
|
|
62
80
|
}, _callee, null, [[2, 17]]);
|
|
63
81
|
}));
|
|
82
|
+
|
|
64
83
|
return function fetchDestinationsForWriteKey(_x) {
|
|
65
84
|
return _ref2.apply(this, arguments);
|
|
66
85
|
};
|
|
@@ -1,55 +1,73 @@
|
|
|
1
1
|
import _regeneratorRuntime from "@babel/runtime/regenerator";
|
|
2
|
+
|
|
2
3
|
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
|
|
4
|
+
|
|
3
5
|
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
|
|
6
|
+
|
|
4
7
|
import { conditionallyLoadAnalytics } from './conditionallyLoadAnalytics';
|
|
8
|
+
import { Consent } from './consent';
|
|
5
9
|
import { fetchDestinationsForWriteKey } from './fetchDestinationsForWriteKey';
|
|
6
10
|
import { mapDestinations } from './mapDestinations';
|
|
7
11
|
import { initializeOneTrust } from './onetrust';
|
|
8
12
|
import { runSegmentSnippet } from './runSegmentSnippet';
|
|
13
|
+
|
|
9
14
|
/**
|
|
10
15
|
* @see README.md for details and usage.
|
|
11
16
|
*/
|
|
12
17
|
export var initializeTrackingIntegrations = /*#__PURE__*/function () {
|
|
13
18
|
var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(_ref) {
|
|
14
|
-
var onError, production, scope, user, writeKey, destinations, _mapDestinations, destinationPreferences, identifyPreferences;
|
|
19
|
+
var onError, production, scope, user, writeKey, optedOut, destinations, _mapDestinations, destinationPreferences, identifyPreferences;
|
|
20
|
+
|
|
15
21
|
return _regeneratorRuntime.wrap(function _callee$(_context) {
|
|
16
22
|
while (1) {
|
|
17
23
|
switch (_context.prev = _context.next) {
|
|
18
24
|
case 0:
|
|
19
25
|
onError = _ref.onError, production = _ref.production, scope = _ref.scope, user = _ref.user, writeKey = _ref.writeKey;
|
|
20
|
-
|
|
26
|
+
optedOut = user === null || user === void 0 ? void 0 : user.opted_out_external_tracking; // 1. Wait 1000ms to allow any other post-hydration logic to run first
|
|
27
|
+
|
|
28
|
+
_context.next = 4;
|
|
21
29
|
return new Promise(function (resolve) {
|
|
22
30
|
return setTimeout(resolve, 1000);
|
|
23
31
|
});
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
|
|
33
|
+
case 4:
|
|
34
|
+
_context.next = 6;
|
|
26
35
|
return initializeOneTrust({
|
|
27
36
|
scope: scope,
|
|
28
37
|
production: production
|
|
29
38
|
});
|
|
30
|
-
case 5:
|
|
31
|
-
// 3. Segment's copy-and-paste snippet is run to load the Segment global library
|
|
32
|
-
runSegmentSnippet();
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
case 6:
|
|
41
|
+
if (optedOut) {
|
|
42
|
+
scope.OnetrustActiveGroups = [Consent.StrictlyNecessary, Consent.Functional];
|
|
43
|
+
} // 3. Segment's copy-and-paste snippet is run to load the Segment global library
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
runSegmentSnippet(); // 4. Destination integrations for Segment are fetched
|
|
47
|
+
|
|
48
|
+
_context.next = 10;
|
|
36
49
|
return fetchDestinationsForWriteKey({
|
|
37
50
|
onError: onError,
|
|
38
51
|
writeKey: writeKey
|
|
39
52
|
});
|
|
40
|
-
|
|
53
|
+
|
|
54
|
+
case 10:
|
|
41
55
|
destinations = _context.sent;
|
|
56
|
+
|
|
42
57
|
if (destinations) {
|
|
43
|
-
_context.next =
|
|
58
|
+
_context.next = 13;
|
|
44
59
|
break;
|
|
45
60
|
}
|
|
61
|
+
|
|
46
62
|
return _context.abrupt("return");
|
|
47
|
-
|
|
63
|
+
|
|
64
|
+
case 13:
|
|
48
65
|
// 5. Those integrations are compared against the user's consent decisions into a list of allowed destinations
|
|
49
66
|
_mapDestinations = mapDestinations({
|
|
50
67
|
consentDecision: scope.OnetrustActiveGroups,
|
|
51
68
|
destinations: destinations
|
|
52
69
|
}), destinationPreferences = _mapDestinations.destinationPreferences, identifyPreferences = _mapDestinations.identifyPreferences; // 6. We load only those allowed destinations using Segment's `analytics.load`
|
|
70
|
+
|
|
53
71
|
conditionallyLoadAnalytics({
|
|
54
72
|
analytics: scope.analytics,
|
|
55
73
|
destinationPreferences: destinationPreferences,
|
|
@@ -57,13 +75,15 @@ export var initializeTrackingIntegrations = /*#__PURE__*/function () {
|
|
|
57
75
|
user: user,
|
|
58
76
|
writeKey: writeKey
|
|
59
77
|
});
|
|
60
|
-
|
|
78
|
+
|
|
79
|
+
case 15:
|
|
61
80
|
case "end":
|
|
62
81
|
return _context.stop();
|
|
63
82
|
}
|
|
64
83
|
}
|
|
65
84
|
}, _callee);
|
|
66
85
|
}));
|
|
86
|
+
|
|
67
87
|
return function initializeTrackingIntegrations(_x) {
|
|
68
88
|
return _ref2.apply(this, arguments);
|
|
69
89
|
};
|
|
@@ -1,35 +1,45 @@
|
|
|
1
1
|
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
2
|
+
|
|
2
3
|
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
|
|
4
|
+
|
|
3
5
|
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
6
|
+
|
|
4
7
|
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
8
|
+
|
|
5
9
|
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); }
|
|
10
|
+
|
|
6
11
|
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
|
|
12
|
+
|
|
7
13
|
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
|
|
14
|
+
|
|
8
15
|
import { Consent } from './consent';
|
|
9
16
|
// The Functional category may need to be added here in the future.
|
|
10
17
|
var targetingCategories = ['Advertising', 'Attribution', 'Email Marketing'];
|
|
11
18
|
var performanceCategories = ['Analytics', 'Customer Success', 'Surveys', 'Heatmaps & Recording'];
|
|
12
19
|
var functionalCategories = ['SMS & Push Notifications'];
|
|
13
|
-
|
|
14
20
|
/**
|
|
15
21
|
* @see https://www.notion.so/codecademy/GDPR-Compliance-141ebcc7ffa542daa0da56e35f482b41
|
|
16
22
|
*/
|
|
23
|
+
|
|
17
24
|
export var mapDestinations = function mapDestinations(_ref) {
|
|
18
25
|
var _ref$consentDecision = _ref.consentDecision,
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
consentDecision = _ref$consentDecision === void 0 ? [Consent.StrictlyNecessary] : _ref$consentDecision,
|
|
27
|
+
destinations = _ref.destinations;
|
|
21
28
|
var destinationPreferences = Object.assign.apply(Object, [{
|
|
22
29
|
'Segment.io': consentDecision.includes(Consent.Functional)
|
|
23
30
|
}].concat(_toConsumableArray(destinations.map(function (dest) {
|
|
24
31
|
if (targetingCategories.includes(dest.category)) {
|
|
25
32
|
return _defineProperty({}, dest.id, consentDecision.includes(Consent.Targeting));
|
|
26
33
|
}
|
|
34
|
+
|
|
27
35
|
if (performanceCategories.includes(dest.category)) {
|
|
28
36
|
return _defineProperty({}, dest.id, consentDecision.includes(Consent.Performance));
|
|
29
37
|
}
|
|
38
|
+
|
|
30
39
|
if (functionalCategories.includes(dest.category)) {
|
|
31
40
|
return _defineProperty({}, dest.id, consentDecision.includes(Consent.Functional));
|
|
32
41
|
}
|
|
42
|
+
|
|
33
43
|
return _defineProperty({}, dest.id, true);
|
|
34
44
|
}))));
|
|
35
45
|
var identifyPreferences = {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import _regeneratorRuntime from "@babel/runtime/regenerator";
|
|
2
|
+
|
|
2
3
|
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
|
|
4
|
+
|
|
3
5
|
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
|
|
6
|
+
|
|
4
7
|
export var initializeOneTrust = /*#__PURE__*/function () {
|
|
5
8
|
var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(_ref) {
|
|
6
9
|
var production, scope, script, style;
|
|
@@ -21,6 +24,7 @@ export var initializeOneTrust = /*#__PURE__*/function () {
|
|
|
21
24
|
return _context.abrupt("return", new Promise(function (resolve) {
|
|
22
25
|
scope.OptanonWrapper = function () {
|
|
23
26
|
var _scope$dataLayer, _script$parentNode;
|
|
27
|
+
|
|
24
28
|
(_scope$dataLayer = scope.dataLayer) !== null && _scope$dataLayer !== void 0 ? _scope$dataLayer : scope.dataLayer = [];
|
|
25
29
|
scope.dataLayer.push({
|
|
26
30
|
event: 'OneTrustGroupsUpdated'
|
|
@@ -29,6 +33,7 @@ export var initializeOneTrust = /*#__PURE__*/function () {
|
|
|
29
33
|
(_script$parentNode = script.parentNode) === null || _script$parentNode === void 0 ? void 0 : _script$parentNode.removeChild(script);
|
|
30
34
|
};
|
|
31
35
|
}));
|
|
36
|
+
|
|
32
37
|
case 11:
|
|
33
38
|
case "end":
|
|
34
39
|
return _context.stop();
|
|
@@ -36,11 +41,11 @@ export var initializeOneTrust = /*#__PURE__*/function () {
|
|
|
36
41
|
}
|
|
37
42
|
}, _callee);
|
|
38
43
|
}));
|
|
44
|
+
|
|
39
45
|
return function initializeOneTrust(_x) {
|
|
40
46
|
return _ref2.apply(this, arguments);
|
|
41
47
|
};
|
|
42
|
-
}();
|
|
43
|
-
|
|
44
|
-
// For now, these three values duplicate theme colors from gamut-styles
|
|
48
|
+
}(); // For now, these three values duplicate theme colors from gamut-styles
|
|
45
49
|
// We don't want to take a full dependency on that package here...
|
|
50
|
+
|
|
46
51
|
var rawStyles = "\n:root {\n --onetrust-brand-purple: #3A10E5;\n --onetrust-color-gray-500: #828285;\n --onetrust-color-white: #fff;\n}\n\n#onetrust-banner-sdk {\n padding: 1rem !important;\n}\n#onetrust-banner-sdk > .ot-sdk-container {\n width: 100% !important;\n}\n#onetrust-banner-sdk > .ot-sdk-container > .ot-sdk-row {\n display: flex !important;\n flex-direction: column !important;\n align-items: center !important;\n max-width: 1436px !important;\n margin: 0 auto !important;\n}\n#onetrust-banner-sdk > .ot-sdk-container > .ot-sdk-row:after {\n content: none !important;\n}\n#onetrust-group-container {\n display: flex !important;\n justify-content: center;\n float: none !important;\n width: 100% !important;\n max-width: 1148px !important;\n margin-left: 0 !important;\n margin-bottom: 0.625rem !important;\n}\n#onetrust-policy,\n#onetrust-policy-text {\n margin: 0 !important;\n font-size: 0.875rem !important;\n line-height: 1.375rem !important;\n text-align: center !important;\n float: none !important;\n}\n#onetrust-policy-text a {\n text-decoration: none;\n line-height: 26px !important;\n margin-left: 0 !important;\n}\n#onetrust-button-group-parent {\n position: relative !important;\n top: initial !important;\n left: initial !important;\n transform: initial !important;\n width: 264px !important;\n margin: 0 !important;\n padding: 0 !important;\n float: none !important;\n}\n#onetrust-button-group {\n display: flex !important;\n margin: 0 !important;\n}\n#onetrust-pc-btn-handler, #onetrust-accept-btn-handler {\n min-width: initial !important;\n padding: 0.375rem 1rem !important;\n margin: 0 !important;\n opacity: 1 !important;\n border-radius: 2px !important;\n line-height: 1.5 !important;\n user-select: none !important;\n font-size: 1rem !important;\n}\n#onetrust-pc-btn-handler:focus, #onetrust-accept-btn-handler:focus {\n box-shadow: 0 0 0 2px var(--onetrust-color-white), 0 0 0 4px var(--onetrust-brand-purple);\n text-decoration: none !important;\n outline: none !important;\n}\n#onetrust-pc-btn-handler{\n color: var(--onetrust-brand-purple) !important;\n border: 1px solid var(--onetrust-brand-purple)!important;\n background: var(--onetrust-color-white) !important\n}\n#onetrust-accept-btn-handler {\n color: var(--onetrust-color-white) !important;\n background: var(--onetrust-brand-purple)!important;\n margin-left: 1rem !important;\n}\n#onetrust-close-btn-container {\n display: none !important;\n}\n\n.pc-logo {\n display: none !important;\n}\n\n#accept-recommended-btn-handler,\n.ot-pc-refuse-all-handler,\n.save-preference-btn-handler {\n margin-left: 4px !important;\n font-size: 14px !important;\n}\n\n#accept-recommended-btn-handler:focus,\n#onetrust-pc-sdk .ot-pc-refuse-all-handler:focus,\n#onetrust-pc-sdk .save-preference-btn-handler:focus {\n box-shadow: 0 0 0 2px var(--onetrust-color-white), 0 0 0 4px var(--onetrust-brand-purple);\n text-decoration: none !important;\n outline: none !important;\n opacity: 1 !important;\n}\n\n.ot-switch-label {\n border: 1px solid var(--onetrust-color-gray-500) !important;\n background-color: var(--onetrust-color-gray-500) !important;\n}\n\n.ot-switch-nob {\n background: var(--onetrust-color-white) !important;\n}\n\n.ot-switch-inner:before {\n background-color: var(--onetrust-brand-purple) !important;\n}\n\n.switch-checkbox:checked+.ot-switch-label .ot-switch-nob {\n border-color: var(--onetrust-brand-purple) !important;\n}\n\n.ot-pc-footer-logo {\n display: none !important;\n}\n\n#onetrust-banner-sdk>.ot-sdk-container {\n overflow: visible !important;\n}\n\n@media (max-width: 30rem) {\n #accept-recommended-btn-handler,\n .ot-pc-refuse-all-handler,\n .save-preference-btn-handler {\n width: 96% !important;\n }\n}\n\n@media (min-width: 37.5rem) {\n #onetrust-banner-sdk {\n padding: 0.875rem 1rem !important;\n }\n}\n@media (min-width: 48rem) {\n #onetrust-banner-sdk {\n padding: 0.875rem 1.25rem !important;\n }\n}\n@media (min-width: 1650px) {\n #onetrust-banner-sdk > .ot-sdk-container > .ot-sdk-row {\n flex-direction: row !important;\n justify-content: space-between !important;\n }\n #onetrust-group-container {\n margin-bottom: 0 !important;\n }\n #onetrust-button-group {\n flex-direction: row !important;\n }\n}\n";
|
|
@@ -2,6 +2,7 @@ import { Consent } from './consent';
|
|
|
2
2
|
export interface UserIntegrationSummary {
|
|
3
3
|
email: string;
|
|
4
4
|
id: string;
|
|
5
|
+
opted_out_external_tracking: boolean;
|
|
5
6
|
}
|
|
6
7
|
export interface SegmentAnalytics {
|
|
7
8
|
identify(id: string, details: Record<string, string>, options: SegmentAnalyticsOptions): void;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codecademy/tracking",
|
|
3
3
|
"description": "Tracking library for Codecademy apps.",
|
|
4
|
-
"version": "0.25.1-alpha.
|
|
4
|
+
"version": "0.25.1-alpha.5b36ce.0",
|
|
5
5
|
"author": "Codecademy Engineering <dev@codecademy.com>",
|
|
6
6
|
"module": "./dist/index.js",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"@babel/cli": "^7.13.10",
|
|
17
17
|
"@types/fetch-mock": "^7.3.3",
|
|
18
18
|
"@types/jest": "^26.0.15",
|
|
19
|
-
"babel-preset-codecademy": "6.0.
|
|
19
|
+
"babel-preset-codecademy": "^6.0.0",
|
|
20
20
|
"fetch-mock": "^9.11.0",
|
|
21
21
|
"jest-fetch-mock": "^3.0.3",
|
|
22
22
|
"ts-jest": "^26.4.1",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "29503baa2905555fad928184d58a78776afdad0c"
|
|
39
39
|
}
|