This commit is contained in:
William Durand 2019-01-09 15:19:21 +01:00 коммит произвёл GitHub
Родитель 65005896d5
Коммит 59fa906a54
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 3 добавлений и 486 удалений

Просмотреть файл

@ -25,7 +25,6 @@ module.exports = {
'enableRequestID',
'enableStrictMode',
'experiments',
'hctEnabled',
'hrefLangsMap',
'isDeployed',
'isDevelopment',

Просмотреть файл

@ -124,7 +124,6 @@ module.exports = {
'enableStrictMode',
'experiments',
'fxaConfig',
'hctEnabled',
'hrefLangsMap',
'isDeployed',
'isDevelopment',
@ -301,9 +300,6 @@ module.exports = {
// send a page view on initialization.
trackingSendInitPageView: true,
// Hybrid Content Telemetry, off by default.
hctEnabled: false,
enablePostCssLoader: true,
// The list of valid client application names.
@ -321,11 +317,10 @@ module.exports = {
// The default app used in the URL.
defaultClientApp: 'firefox',
// Dynamic JS chunk patterns to exclude. If these strings match any part of the JS file
// leaf name they will be excluded from being output in the HTML.
// Dynamic JS chunk patterns to exclude. If these strings match any part of
// the JS file leaf name they will be excluded from being output in the HTML.
jsChunkExclusions: [
'i18n',
'disco-hct',
],
fxaConfig: null,

Просмотреть файл

@ -25,6 +25,4 @@ module.exports = {
// https://sentry.prod.mozaws.net/operations/addons-frontend-disco-dev/
publicSentryDsn: 'https://560fc81d9fd14266b99bda032de23c52@sentry.prod.mozaws.net/184',
hctEnabled: true,
};

Просмотреть файл

@ -1,7 +1,4 @@
module.exports = {
trackingEnabled: false,
loggingLevel: 'debug',
// This requires further manual configuration to collect data.
// Please see docs/telemetry.md for more info.
hctEnabled: true,
};

Просмотреть файл

@ -1,60 +0,0 @@
# Hybrid Content Telemetry
Telemetry is being introduced to the discovery pane to replace Google Analytics.
The following events are logged to telemetry if:
- HCT is enabled for the host
- Telemetry collection is allowed by the end-user.
| Category | Method | Action | Value | Extra | This is logged when... |
| ----------------- | ----------------- | ------------------------- | ------------------- | -------------------- | --------------------------------------- |
| disco.interaction | addon_click | [addon/theme/statictheme] | [Add-on name] | { origin: [origin] } | An add-on link is clicked |
| disco.interaction | download_failed | [addon/theme/statictheme] | [Add-on name] | { origin: [origin] } | The download of an extension has failed |
| disco.interaction | enabled | [addon/theme/statictheme] | [Add-on name] | { origin: [origin] } | Add-on is enabled |
| disco.interaction | installed | [addon/theme/statictheme] | [Add-on name] | { origin: [origin] } | Add-on is installed |
| disco.interaction | install_cancelled | [addon/theme/statictheme] | [Add-on name] | { origin: [origin] } | Add-on install is cancelled |
| disco.interaction | install_started | [addon/theme/statictheme] | [Add-on name] | { origin: [origin] } | Add-on install has started |
| disco.interaction | uninstalled | [addon/theme/statictheme] | [Add-on name] | { origin: [origin] } | Add-on uninstalled |
| disco.interaction | navigation_click | click | [Click description] | { origin: [origin] } | When user clicks "Find more Add-ons" |
## Testing in your local development environment
Here are the steps to test collection locally:
- `hctEnabled` is set to `true` by default in `config/development-disco.js`.
- Run `yarn disco:https` to start the disco app because HCT requires HTTPS.
- Go to `about:config` and enable `devtools.chrome.enabled` so that the browser console has the CLI enabled.
- Open the Browser Console (and not the classic devtools) and type:
```javascript
let hostURI = Services.io.newURI('https://example.com:3000');
Services.perms.add(hostURI, 'hc_telemetry', Services.perms.ALLOW_ACTION);
```
## Testing on -dev (hosted environment)
You'll need to enable installs from -dev before enabling collection. You can skip this step if it's already been done.
**NOTE: It's recommended you do these settings changes in a new profile as changing to the -dev cert will mark all existing add-ons as invalid.**
- Right click in `about:config`, select `new` and then add `xpinstall.signatures.dev-root` as `Boolean`. It should be `true`.
- Right click in `about:config`, select `new` and add `extensions.webapi.testing` as `Boolean`. It should be `true`.
- Restart the browser.
Now enable collection on -dev:
- Open the Browser Console (and not the classic devtools) and type:
```javascript
let hostURI = Services.io.newURI('https://discovery.addons-dev.allizom.org');
Services.perms.add(hostURI, 'hc_telemetry', Services.perms.ALLOW_ACTION);
```
## Viewing data collected
- Navigate to `about:telemetry#events-tab` and select the `dynamic` filter (top-right dropdown)
If there's no data shown, interact with the disco pane and refresh the page (you will need to reselect dynamic) in the filter.
Here's the [link to the -dev disco pane](https://discovery.addons-dev.allizom.org/en-US/firefox/discovery/pane/57.0/Darwin/normal)

Просмотреть файл

@ -203,9 +203,9 @@
"homepage": "https://github.com/mozilla/addons-frontend#readme",
"dependencies": {
"@babel/polyfill": "7.2.5",
"base62": "2.0.0",
"@loadable/component": "5.2.2",
"@loadable/server": "5.2.2",
"base62": "2.0.0",
"base64url": "3.0.1",
"better-npm-run": "0.1.1",
"chokidar": "2.0.4",
@ -236,7 +236,6 @@
"localforage": "1.7.3",
"lodash.debounce": "4.0.8",
"moment": "2.23.0",
"mozilla-hybrid-content-telemetry": "1.2.1",
"mozilla-version-comparator": "1.0.2",
"nano-time": "1.0.0",
"normalize.css": "8.0.1",

Просмотреть файл

@ -234,38 +234,6 @@ export const SURVEY_ACTION_DISMISSED = 'Dismissed survey notice';
export const SURVEY_ACTION_SHOWN = 'Shown survey notice';
export const SURVEY_ACTION_VISITED = 'Visited survey';
// Mapping of GA Categories to HCT compatible methods.
// All the HCT methods must be under 20 chars
// and should match /^[a-z0-9]{1}[a-z0-9_]+[a-z0-9]{1}$/i
// See https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/events.html#limits
// for more details.
export const HCT_DISCO_CATEGORY = 'disco.interaction';
export const HCT_ADDON_CLICK = 'addon_click';
export const HCT_ADDON_ENABLED = 'enabled';
export const HCT_ADDON_INSTALLED = 'installed';
export const HCT_ADDON_INSTALL_CANCELLED = 'cancelled';
export const HCT_ADDON_DOWNLOAD_FAILED = 'download_failed';
export const HCT_ADDON_INSTALL_STARTED = 'install_started';
export const HCT_ADDON_NAVIGATION_CLICK = 'navigation_click';
export const HCT_ADDON_UNINSTALLED = 'uninstalled';
export const HCT_METHOD_MAPPING = {
[CLICK_CATEGORY]: HCT_ADDON_CLICK,
[DISCO_NAVIGATION_CATEGORY]: HCT_ADDON_NAVIGATION_CLICK,
[INSTALL_EXTENSION_CATEGORY]: HCT_ADDON_INSTALLED,
[INSTALL_THEME_CATEGORY]: HCT_ADDON_INSTALLED,
[UNINSTALL_EXTENSION_CATEGORY]: HCT_ADDON_UNINSTALLED,
[UNINSTALL_THEME_CATEGORY]: HCT_ADDON_UNINSTALLED,
[ENABLE_EXTENSION_CATEGORY]: HCT_ADDON_ENABLED,
[ENABLE_THEME_CATEGORY]: HCT_ADDON_ENABLED,
[INSTALL_CANCELLED_EXTENSION_CATEGORY]: HCT_ADDON_INSTALL_CANCELLED,
[INSTALL_CANCELLED_THEME_CATEGORY]: HCT_ADDON_INSTALL_CANCELLED,
[INSTALL_STARTED_EXTENSION_CATEGORY]: HCT_ADDON_INSTALL_STARTED,
[INSTALL_STARTED_THEME_CATEGORY]: HCT_ADDON_INSTALL_STARTED,
[INSTALL_DOWNLOAD_FAILED_EXTENSION_CATEGORY]: HCT_ADDON_DOWNLOAD_FAILED,
[INSTALL_DOWNLOAD_FAILED_THEME_CATEGORY]: HCT_ADDON_DOWNLOAD_FAILED,
};
// Error used to know that the setEnable method on addon is
// not available.
export const SET_ENABLE_NOT_AVAILABLE = 'SET_ENABLE_NOT_AVAILABLE';

Просмотреть файл

@ -11,12 +11,9 @@ import {
ADDON_TYPE_OPENSEARCH,
ADDON_TYPE_STATIC_THEME,
ADDON_TYPE_THEME,
DISCO_NAVIGATION_ACTION,
ENABLE_ACTION,
ENABLE_EXTENSION_CATEGORY,
ENABLE_THEME_CATEGORY,
HCT_DISCO_CATEGORY,
HCT_METHOD_MAPPING,
INSTALL_CANCELLED_ACTION,
INSTALL_CANCELLED_EXTENSION_CATEGORY,
INSTALL_CANCELLED_THEME_CATEGORY,
@ -39,15 +36,6 @@ import {
import log from 'core/logger';
import { convertBoolean, isTheme } from 'core/utils';
const telemetryMethodKeys = Object.keys(HCT_METHOD_MAPPING);
const telemetryMethodValues = Object.values(HCT_METHOD_MAPPING);
export const telemetryObjects = [
DISCO_NAVIGATION_ACTION,
TRACKING_TYPE_EXTENSION,
TRACKING_TYPE_THEME,
TRACKING_TYPE_STATIC_THEME,
];
type IsDoNoTrackEnabledParams = {|
_log: typeof log,
_navigator: ?typeof navigator,
@ -94,11 +82,6 @@ export class Tracking {
// Tracking ID
id: string;
// Hybrid Content Telemetry
hctEnabled: boolean;
hctInitPromise: Promise<null | Object>;
constructor({
_config = config,
_isDoNotTrackEnabled = isDoNotTrackEnabled,
@ -110,7 +93,6 @@ export class Tracking {
this._log = log;
this.logPrefix = '[GA]'; // this gets updated below
this.id = _config.get('trackingId');
this.hctEnabled = false;
if (!convertBoolean(_config.get('trackingEnabled'))) {
this.log('GA disabled because trackingEnabled was false');
@ -154,54 +136,6 @@ export class Tracking {
// (addons-frontend vs addons-server) is being used in analytics.
ga('set', 'dimension3', 'addons-frontend');
}
// Attempt to enable Hybrid content telemetry.
this.hctInitPromise = this.initHCT({ _config });
}
async initHCT({ _config = config }: {| _config: typeof config |} = {}) {
const hctEnabled = _config.get('hctEnabled');
if (typeof window !== 'undefined' && hctEnabled === true) {
log.info('Setting up the Hybrid Content Telemetry lib');
// Note: special webpack comments must be after the module name or
// babel-plugin-dynamic-import-node will blow-up.
try {
// prettier-ignore
const hybridContentTelemetry = await import(
'mozilla-hybrid-content-telemetry/HybridContentTelemetry-lib'
/* webpackChunkName: "disco-hct" */
);
await hybridContentTelemetry.initPromise();
let logHctReason;
if (!hybridContentTelemetry) {
logHctReason =
'HCT disabled because hctEnabled or hct object is not available';
} else {
logHctReason = 'HCT enabled';
this.hctEnabled = true;
hybridContentTelemetry.registerEvents(HCT_DISCO_CATEGORY, {
click: {
methods: telemetryMethodValues,
objects: telemetryObjects,
extra_keys: ['origin'],
},
});
}
// Update the logging prefix to include HCT status.
this.logPrefix = oneLine`[GA: ${this.trackingEnabled ? 'ON' : 'OFF'}
| HCT: ${this.hctEnabled ? 'ON' : 'OFF'}]`;
this.log(logHctReason);
return hybridContentTelemetry;
} catch (err) {
// eslint-disable-next-line amo/only-log-strings
log.error('Initialization failed', err);
}
}
// eslint-disable-next-line amo/only-log-strings
log.info('Not importing the HCT lib, hctEnabled:', hctEnabled);
return Promise.resolve(null);
}
log(...args: Array<mixed>) {
@ -217,53 +151,6 @@ export class Tracking {
}
}
async _hct(
data: {| method: string, object: string, value?: string |},
{ _window = window }: {| _window: typeof window |} = {},
) {
const hybridContentTelemetry = await this.hctInitPromise;
if (hybridContentTelemetry) {
const canUpload = hybridContentTelemetry.canUpload();
if (canUpload === true) {
invariant(
telemetryMethodKeys.includes(data.method),
`Method mapping for "${
data.method
}" is missing from HCT_METHOD_MAPPING`,
);
const method = HCT_METHOD_MAPPING[data.method];
const { object } = data;
invariant(
telemetryObjects.includes(object),
`Object "${object}" must be one of the registered values: ${telemetryObjects.join(
',',
)}`,
);
hybridContentTelemetry.recordEvent(
HCT_DISCO_CATEGORY,
method,
object,
data.value,
{
origin:
typeof _window !== 'undefined' &&
_window.location &&
_window.location.origin
? _window.location.origin
: null,
},
);
} else {
this.log(
`Not logging to telemetry because canUpload() returned: ${canUpload}`,
);
}
} else {
this.log(oneLine`Not logging to telemetry since hctEnabled is
${this.hctEnabled ? 'enabled' : 'disabled'}`);
}
}
/*
* Param Type Required Description
* obj.category String Yes Typically the object that
@ -299,12 +186,6 @@ export class Tracking {
eventValue: value,
};
this._ga('send', data);
// Hybrid content telemetry maps to the data used for GA.
this._hct({
method: category,
object: action,
value: label,
});
this.log('sendEvent', JSON.stringify(data));
}

Просмотреть файл

@ -109,12 +109,6 @@ describe(__filename, () => {
expect(js.exists()).toEqual(false);
});
it('does not render hct lib js in the assets list', () => {
const root = render();
const js = root.find('script[integrity="sha512-disco-hct-js"]');
expect(js.exists()).toEqual(false);
});
it('renders css with SRI when present', () => {
const root = render();
const styleSheets = root.find({ rel: 'stylesheet' });

Просмотреть файл

@ -10,7 +10,6 @@ export const fakeAssets = {
javascript: {
disco: '/foo/disco-blah.js',
search: '/search-blah.js',
hct: '/blah/disco-hct.js',
i18n: '/blah/disco-i18n.js',
},
};
@ -20,7 +19,6 @@ export const fakeSRIData = {
'search-blah.css': 'sha512-search-css',
'disco-blah.js': 'sha512-disco-js',
'search-blah.js': 'sha512-search-js',
'disco-hct.js': 'sha512-disco-hct-js',
'disco-i18n.js': 'sha512-disco-i18n-js',
};

Просмотреть файл

@ -1,12 +1,9 @@
/* global window */
import hct from 'mozilla-hybrid-content-telemetry/HybridContentTelemetry-lib';
import {
Tracking,
isDoNotTrackEnabled,
getAddonEventCategory,
getAddonTypeForTracking,
telemetryObjects,
} from 'core/tracking';
import {
ADDON_TYPE_DICT,
@ -20,14 +17,6 @@ import {
ENABLE_ACTION,
ENABLE_EXTENSION_CATEGORY,
ENABLE_THEME_CATEGORY,
HCT_ADDON_DOWNLOAD_FAILED,
HCT_ADDON_ENABLED,
HCT_ADDON_INSTALLED,
HCT_ADDON_INSTALL_CANCELLED,
HCT_ADDON_INSTALL_STARTED,
HCT_ADDON_UNINSTALLED,
HCT_DISCO_CATEGORY,
HCT_METHOD_MAPPING,
INSTALL_ACTION,
INSTALL_CANCELLED_ACTION,
INSTALL_CANCELLED_EXTENSION_CATEGORY,
@ -440,80 +429,6 @@ describe(__filename, () => {
});
});
describe('HCT identifiers', () => {
const telemetryRegex = /^[a-z0-9]{1}[a-z0-9_]+[a-z0-9]{1}$/i;
// Set is to de-dupe the values since we map multiple keys to the same
// value in Hybrid Content Telemetry.
it.each(Array.from(new Set(Object.values(HCT_METHOD_MAPPING))))(
'should ensure hct method "%s" meets HCT identifier requirements',
(idString) => {
expect(idString).toMatch(telemetryRegex);
expect(idString.length).toBeLessThanOrEqual(20);
},
);
it.each(telemetryObjects)(
'should ensure hct object (%s) meets HCT identifier requirements',
(action) => {
expect(action).toMatch(telemetryRegex);
expect(action.length).toBeLessThanOrEqual(20);
},
);
it('should map to HCT_ADDON_INSTALLED correctly', () => {
expect(HCT_METHOD_MAPPING[INSTALL_EXTENSION_CATEGORY]).toBe(
HCT_ADDON_INSTALLED,
);
expect(HCT_METHOD_MAPPING[INSTALL_THEME_CATEGORY]).toBe(
HCT_ADDON_INSTALLED,
);
});
it('should map to HCT_ADDON_UNINSTALLED correctly', () => {
expect(HCT_METHOD_MAPPING[UNINSTALL_EXTENSION_CATEGORY]).toBe(
HCT_ADDON_UNINSTALLED,
);
expect(HCT_METHOD_MAPPING[UNINSTALL_THEME_CATEGORY]).toBe(
HCT_ADDON_UNINSTALLED,
);
});
it('should map to HCT_ADDON_DOWNLOAD_FAILED correctly', () => {
expect(
HCT_METHOD_MAPPING[INSTALL_DOWNLOAD_FAILED_EXTENSION_CATEGORY],
).toBe(HCT_ADDON_DOWNLOAD_FAILED);
expect(HCT_METHOD_MAPPING[INSTALL_DOWNLOAD_FAILED_THEME_CATEGORY]).toBe(
HCT_ADDON_DOWNLOAD_FAILED,
);
});
it('should map to HCT_ADDON_ENABLED correctly', () => {
expect(HCT_METHOD_MAPPING[ENABLE_EXTENSION_CATEGORY]).toBe(
HCT_ADDON_ENABLED,
);
expect(HCT_METHOD_MAPPING[ENABLE_THEME_CATEGORY]).toBe(HCT_ADDON_ENABLED);
});
it('should map to HCT_ADDON_INSTALL_CANCELLED correctly', () => {
expect(HCT_METHOD_MAPPING[INSTALL_CANCELLED_EXTENSION_CATEGORY]).toBe(
HCT_ADDON_INSTALL_CANCELLED,
);
expect(HCT_METHOD_MAPPING[INSTALL_CANCELLED_THEME_CATEGORY]).toBe(
HCT_ADDON_INSTALL_CANCELLED,
);
});
it('should map to HCT_ADDON_INSTALL_STARTED correctly', () => {
expect(HCT_METHOD_MAPPING[INSTALL_STARTED_EXTENSION_CATEGORY]).toBe(
HCT_ADDON_INSTALL_STARTED,
);
expect(HCT_METHOD_MAPPING[INSTALL_STARTED_THEME_CATEGORY]).toBe(
HCT_ADDON_INSTALL_STARTED,
);
});
});
describe('Tracking constants should not be changed or it risks breaking tracking stats', () => {
it('should not change the tracking constant for invalid', () => {
expect(TRACKING_TYPE_INVALID).toEqual('invalid');
@ -567,143 +482,4 @@ describe(__filename, () => {
expect(DISCO_NAVIGATION_CATEGORY).toEqual('Discovery Navigation');
});
});
describe('Hybrid Content Telemetry', () => {
let importStub;
let registerEventsSpy;
beforeEach(() => {
importStub = sinon.stub(hct, 'initPromise').callsFake(() => {
return Promise.resolve(hct);
});
registerEventsSpy = sinon.spy(hct, 'registerEvents');
});
afterEach(() => {
importStub.restore();
registerEventsSpy.restore();
});
it('should return null from the init promise if hctEnabled is false', async () => {
const tracking = createTracking({
configOverrides: { hctEnabled: false },
});
const hctLib = await tracking.hctInitPromise;
expect(hctLib).toEqual(null);
});
it('should return hct object from the init promise if hctEnabled is true', async () => {
const tracking = createTracking({
configOverrides: { hctEnabled: true },
});
const hctLib = await tracking.hctInitPromise;
expect(hctLib).toHaveProperty('canUpload');
expect(hctLib).toHaveProperty('initPromise');
expect(hctLib).toHaveProperty('recordEvent');
expect(hctLib).toHaveProperty('registerEvents');
});
it('should call registerEvents if hctEnabled is true', async () => {
const tracking = createTracking({
configOverrides: { hctEnabled: true },
});
await tracking.hctInitPromise;
sinon.assert.calledOnce(registerEventsSpy);
});
});
describe('Hybrid Content Telemetry Events', () => {
let importStub;
let canUploadStub;
let recordEventSpy;
const trackingData = {
method: INSTALL_EXTENSION_CATEGORY,
object: TRACKING_TYPE_EXTENSION,
value: 'value',
};
beforeEach(() => {
importStub = sinon.stub(hct, 'initPromise').callsFake(() => {
return Promise.resolve(hct);
});
canUploadStub = sinon.stub(hct, 'canUpload');
recordEventSpy = sinon.spy(hct, 'recordEvent');
});
afterEach(() => {
importStub.restore();
canUploadStub.restore();
recordEventSpy.restore();
});
it('should not call recordEvent if canUpload returns false', async () => {
canUploadStub.callsFake(() => false);
const tracking = createTracking({
configOverrides: { hctEnabled: true },
});
await tracking._hct(trackingData);
sinon.assert.notCalled(recordEventSpy);
});
it('should call recordEvent if canUpload is true', async () => {
canUploadStub.callsFake(() => true);
const tracking = createTracking({
configOverrides: { hctEnabled: true },
});
await tracking._hct(trackingData);
sinon.assert.calledOnce(recordEventSpy);
sinon.assert.calledWith(
recordEventSpy,
HCT_DISCO_CATEGORY,
HCT_METHOD_MAPPING[INSTALL_EXTENSION_CATEGORY],
TRACKING_TYPE_EXTENSION,
'value',
{ origin: 'http://localhost' },
);
});
it('should record null origin if origin is not available', async () => {
canUploadStub.callsFake(() => true);
const tracking = createTracking({
configOverrides: { hctEnabled: true },
});
await tracking._hct(trackingData, {
_window: {
location: {},
},
});
sinon.assert.calledOnce(recordEventSpy);
sinon.assert.calledWith(
recordEventSpy,
HCT_DISCO_CATEGORY,
HCT_METHOD_MAPPING[INSTALL_EXTENSION_CATEGORY],
TRACKING_TYPE_EXTENSION,
'value',
{ origin: null },
);
});
it('should record null origin if window is empty object', async () => {
canUploadStub.callsFake(() => true);
const tracking = createTracking({
configOverrides: { hctEnabled: true },
});
await tracking._hct(trackingData, {
_window: {},
});
sinon.assert.calledOnce(recordEventSpy);
sinon.assert.calledWith(
recordEventSpy,
HCT_DISCO_CATEGORY,
HCT_METHOD_MAPPING[INSTALL_EXTENSION_CATEGORY],
TRACKING_TYPE_EXTENSION,
'value',
{ origin: null },
);
});
});
});

Просмотреть файл

@ -1,23 +0,0 @@
import { readFile } from 'fs';
import { HCT_METHOD_MAPPING } from 'core/constants';
describe(__filename, () => {
describe('Tracking docs', () => {
let markdown;
beforeAll((done) => {
readFile('docs/telemetry.md', 'utf8', (err, data) => {
markdown = data;
done();
});
});
it.each(Object.values(HCT_METHOD_MAPPING))(
'should have documented %s in docs/telemetry.md',
(method) => {
expect(markdown.indexOf(method) > -1).toBe(true);
},
);
});
});

Просмотреть файл

@ -9308,11 +9308,6 @@ move-concurrently@^1.0.1:
rimraf "^2.5.4"
run-queue "^1.0.3"
mozilla-hybrid-content-telemetry@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/mozilla-hybrid-content-telemetry/-/mozilla-hybrid-content-telemetry-1.2.1.tgz#8d4d7c842a19d123139c157bfc72b9b01bb5b8b9"
integrity sha512-0CkeJVKVb0rfqfBvSvKokPNlIOf4Z2Dpik5n+aL9v/NOkiDTxUJJ0lDCwJj4QL1a+oTdyYnJOt71Av3A2VuQpw==
mozilla-version-comparator@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/mozilla-version-comparator/-/mozilla-version-comparator-1.0.2.tgz#f86731e70c15d1ff5eb288d13b4db8d1e605f7fc"