Remove JS build step and tooling

This commit is contained in:
Rehan Dalal 2020-06-08 19:58:00 -04:00
Родитель af64ac2951
Коммит cd59cf6b8b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 410D198EEF339E0B
17 изменённых файлов: 1336 добавлений и 3118 удалений

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

@ -1,43 +0,0 @@
env:
es6: true
extends:
- airbnb
plugins:
- jasmine
globals:
PRODUCTION: false
DEVELOPMENT: false
rules:
arrow-parens: [warn, as-needed]
comma-dangle: [warn]
class-methods-use-this: [off]
no-console: [off]
no-continue: [off]
no-mixed-operators: [warn, { allowSamePrecedence: true }]
no-param-reassign: [warn, { props: false }]
no-prototype-builtins: [off]
no-restricted-syntax: [off]
no-throw-literal: [off]
no-underscore-dangle: [off]
no-use-before-define: [warn, { functions: false, classes: false }]
prefer-const: [warn]
no-plusplus: [off]
no-await-in-loop: [off]
import/no-extraneous-dependencies: [error, {devDependencies: true}]
import/no-mutable-exports: [off]
import/no-named-as-default: [off]
import/order:
- error
- newlines-between: always-and-inside-groups
groups:
- [builtin, external]
- [internal]
- [parent, index, sibling]
generator-star-spacing: [warn]

1
.gitignore поставляемый
Просмотреть файл

@ -2,7 +2,6 @@
db.sqlite3 db.sqlite3
.DS_Store .DS_Store
__pycache__/ __pycache__/
/assets/
/static/ /static/
/media/ /media/
GeoLite2-Country.mmdb GeoLite2-Country.mmdb

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

@ -1,31 +0,0 @@
{
"extends": "stylelint-config-standard",
"plugins": [
"stylelint-order",
],
"rules": {
"color-hex-case": "upper",
"max-empty-lines": 2,
"order/declaration-block-order": [
{
"type": "at-rule",
"name": "import",
}, {
"type": "at-rule",
"name": "include",
}, {
"type": "at-rule",
"name": "extend",
},
"custom-properties",
"dollar-variables",
"declarations",
"rules",
"at-rules",
],
"order/declaration-block-properties-alphabetical-order": true,
"unit-no-unknown": [true, {
"ignoreUnits": ["/fr/"],
}],
}
}

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

@ -9,9 +9,6 @@ actions:
include: "*.py" include: "*.py"
exclude: "docs/" exclude: "docs/"
yarn-audit:
run: yarn audit
missing-migirations: missing-migirations:
include: "*.py" include: "*.py"
run: | run: |

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

@ -33,8 +33,7 @@ COPY ./poetry.lock /app/poetry.lock
RUN poetry install --no-dev --no-root --no-interaction --verbose RUN poetry install --no-dev --no-root --no-interaction --verbose
COPY . /app COPY . /app
RUN NODE_ENV=production yarn build && \ RUN DJANGO_CONFIGURATION=Build python ./manage.py collectstatic --no-input && \
DJANGO_CONFIGURATION=Build python ./manage.py collectstatic --no-input && \
mkdir -p media && chown app:app media mkdir -p media && chown app:app media
USER app USER app

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

@ -34,8 +34,7 @@ RUN poetry install --no-root --extras docs
COPY . /app COPY . /app
RUN NODE_ENV=production yarn build && \ RUN DJANGO_CONFIGURATION=Build python ./manage.py collectstatic --no-input && \
DJANGO_CONFIGURATION=Build python ./manage.py collectstatic --no-input && \
mkdir -p media mkdir -p media
ENV DJANGO_SETTINGS_MODULE=normandy.settings \ ENV DJANGO_SETTINGS_MODULE=normandy.settings \

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

@ -0,0 +1,183 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./client/actions/console-log/index.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./client/actions/console-log/index.js":
/*!*********************************************!*\
!*** ./client/actions/console-log/index.js ***!
\*********************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return ConsoleLogAction; });
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../utils */ "./client/actions/utils.js");
class ConsoleLogAction extends _utils__WEBPACK_IMPORTED_MODULE_0__["Action"] {
async execute() {
this.normandy.log(this.recipe.arguments.message, 'info');
}
}
Object(_utils__WEBPACK_IMPORTED_MODULE_0__["registerAction"])('console-log', ConsoleLogAction);
/***/ }),
/***/ "./client/actions/utils.js":
/*!*********************************!*\
!*** ./client/actions/utils.js ***!
\*********************************/
/*! exports provided: Action, registerAction, registerAsyncCallback */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Action", function() { return Action; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "registerAction", function() { return registerAction; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "registerAsyncCallback", function() { return registerAsyncCallback; });
class Action {
constructor(normandy, recipe) {
this.normandy = normandy;
this.recipe = recipe;
}
}
// Attempt to find the global registerAction, and fall back to a noop if it's
// not available.
const registerAction = (
(global && global.registerAction)
|| (window && window.registerAction)
|| function registerAction() {}
);
// Same as above, for registerAsyncCallback
const registerAsyncCallback = (
(global && global.registerAsyncCallback)
|| (window && window.registerAsyncCallback)
|| function registerAsyncCallback() {}
);
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/global.js */ "./node_modules/webpack/buildin/global.js")))
/***/ }),
/***/ "./node_modules/webpack/buildin/global.js":
/*!***********************************!*\
!*** (webpack)/buildin/global.js ***!
\***********************************/
/*! no static exports found */
/***/ (function(module, exports) {
var g;
// This works in non-strict mode
g = (function() {
return this;
})();
try {
// This works if eval is allowed (see CSP)
g = g || new Function("return this")();
} catch (e) {
// This works if the window reference is available
if (typeof window === "object") g = window;
}
// g can still be undefined, but nothing to do about it...
// We return undefined, instead of nothing here, so it's
// easier to handle this case. if(!global) { ...}
module.exports = g;
/***/ })
/******/ });
//# sourceMappingURL=console-log.js.map

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -0,0 +1,252 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./client/actions/opt-out-study/index.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./client/actions/opt-out-study/index.js":
/*!***********************************************!*\
!*** ./client/actions/opt-out-study/index.js ***!
\***********************************************/
/*! exports provided: default, postExecutionHook */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return OptOutStudyAction; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "postExecutionHook", function() { return postExecutionHook; });
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../utils */ "./client/actions/utils.js");
const SHIELD_OPT_OUT_PREF = 'app.shield.optoutstudies.enabled';
let seenRecipeIds = [];
/**
* Enrolls a user in an opt-out study, in which we install an add-on which
* manages branch selection, changes to Firefox, etc.
*/
class OptOutStudyAction extends _utils__WEBPACK_IMPORTED_MODULE_0__["Action"] {
async execute() {
const recipeId = this.recipe.id;
const {
name, description, addonUrl, isEnrollmentPaused,
} = this.recipe.arguments;
const { preferences, studies } = this.normandy;
// Exit early if we're on an incompatible client.
if (studies === undefined) {
this.normandy.log('Client does not support studies, aborting.', 'info');
return;
}
// Check opt-out preference
if (preferences && !preferences.getBool(SHIELD_OPT_OUT_PREF, false)) {
this.normandy.log('User has opted-out of opt-out experiments, aborting.', 'info');
return;
}
seenRecipeIds.push(recipeId);
const hasStudy = await studies.has(recipeId);
if (isEnrollmentPaused) {
this.normandy.log(`Enrollment is paused for recipe ${recipeId}`, 'debug');
} else if (hasStudy) {
this.normandy.log(`Study for recipe ${recipeId} already exists`, 'debug');
} else {
this.normandy.log(`Starting study for recipe ${recipeId}`, 'debug');
await studies.start({
recipeId,
name,
description,
addonUrl,
});
}
}
}
Object(_utils__WEBPACK_IMPORTED_MODULE_0__["registerAction"])('opt-out-study', OptOutStudyAction);
/**
* Finds active studies that were not stored in the seenRecipeIds list during
* action execution, and stops them.
*/
async function postExecutionHook(normandy) {
const { studies } = normandy;
// Exit early if we're on an incompatible client.
if (studies === undefined) {
normandy.log('Client does not support studies, aborting.', 'info');
return;
}
// If any of the active studies were not seen during a run, stop them.
const activeStudies = (await studies.getAll()).filter(study => study.active);
for (const study of activeStudies) {
if (!seenRecipeIds.includes(study.recipeId)) {
normandy.log(`Stopping study for recipe ${study.recipeId}.`, 'debug');
try {
await studies.stop(study.recipeId, 'recipe-not-seen');
} catch (err) {
normandy.log(`Error while stopping study for recipe ${study.recipeId}: ${err}`, 'error');
}
}
}
}
Object(_utils__WEBPACK_IMPORTED_MODULE_0__["registerAsyncCallback"])('postExecution', postExecutionHook);
/***/ }),
/***/ "./client/actions/utils.js":
/*!*********************************!*\
!*** ./client/actions/utils.js ***!
\*********************************/
/*! exports provided: Action, registerAction, registerAsyncCallback */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Action", function() { return Action; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "registerAction", function() { return registerAction; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "registerAsyncCallback", function() { return registerAsyncCallback; });
class Action {
constructor(normandy, recipe) {
this.normandy = normandy;
this.recipe = recipe;
}
}
// Attempt to find the global registerAction, and fall back to a noop if it's
// not available.
const registerAction = (
(global && global.registerAction)
|| (window && window.registerAction)
|| function registerAction() {}
);
// Same as above, for registerAsyncCallback
const registerAsyncCallback = (
(global && global.registerAsyncCallback)
|| (window && window.registerAsyncCallback)
|| function registerAsyncCallback() {}
);
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/global.js */ "./node_modules/webpack/buildin/global.js")))
/***/ }),
/***/ "./node_modules/webpack/buildin/global.js":
/*!***********************************!*\
!*** (webpack)/buildin/global.js ***!
\***********************************/
/*! no static exports found */
/***/ (function(module, exports) {
var g;
// This works in non-strict mode
g = (function() {
return this;
})();
try {
// This works if eval is allowed (see CSP)
g = g || new Function("return this")();
} catch (e) {
// This works if the window reference is available
if (typeof window === "object") g = window;
}
// g can still be undefined, but nothing to do about it...
// We return undefined, instead of nothing here, so it's
// easier to handle this case. if(!global) { ...}
module.exports = g;
/***/ })
/******/ });
//# sourceMappingURL=opt-out-study.js.map

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -0,0 +1,298 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./client/actions/preference-experiment/index.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./client/actions/preference-experiment/index.js":
/*!*******************************************************!*\
!*** ./client/actions/preference-experiment/index.js ***!
\*******************************************************/
/*! exports provided: default, postExecutionHook */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return PreferenceExperimentAction; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "postExecutionHook", function() { return postExecutionHook; });
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../utils */ "./client/actions/utils.js");
const SHIELD_OPT_OUT_PREF = 'app.shield.optoutstudies.enabled';
let seenExperimentNames = [];
/**
* Enrolls a user in a preference experiment, in which we assign the user to an
* experiment branch and modify a preference temporarily to measure how it
* affects Firefox via Telemetry.
*/
class PreferenceExperimentAction extends _utils__WEBPACK_IMPORTED_MODULE_0__["Action"] {
async execute() {
const {
branches,
isHighPopulation,
isEnrollmentPaused,
preferenceBranchType,
preferenceName,
preferenceType,
slug,
} = this.recipe.arguments;
const experiments = this.normandy.preferenceExperiments;
// Exit early if we're on an incompatible client.
if (experiments === undefined) {
this.normandy.log('Client does not support preference experiments, aborting.', 'info');
return;
}
// Check opt-out preference
const { preferences } = this.normandy;
if (preferences && !preferences.getBool(SHIELD_OPT_OUT_PREF, false)) {
this.normandy.log('User has opted-out of preference experiments, aborting.', 'info');
return;
}
seenExperimentNames.push(slug);
// If the experiment doesn't exist yet, enroll!
const hasSlug = await experiments.has(slug);
if (!hasSlug) {
// If there's already an active experiment using this preference, abort.
const activeExperiments = await experiments.getAllActive();
const hasConflicts = activeExperiments.some(exp => exp.preferenceName === preferenceName);
if (hasConflicts) {
this.normandy.log(
`Experiment ${slug} ignored; another active experiment is already using the
${preferenceName} preference.`, 'warn',
);
return;
}
// Determine if enrollment is currently paused for this experiment.
if (isEnrollmentPaused) {
this.normandy.log(`Enrollment is paused for experiment "${slug}"`, 'debug');
return;
}
// Otherwise, enroll!
const branch = await this.chooseBranch(branches);
const experimentType = isHighPopulation ? 'exp-highpop' : 'exp';
await experiments.start({
name: slug,
branch: branch.slug,
preferenceName,
preferenceValue: branch.value,
preferenceBranchType,
preferenceType,
experimentType,
});
} else {
// If the experiment exists, and isn't expired, bump the lastSeen date.
const experiment = await experiments.get(slug);
if (experiment.expired) {
this.normandy.log(`Experiment ${slug} has expired, aborting.`, 'debug');
} else {
await experiments.markLastSeen(slug);
}
}
}
async chooseBranch(branches) {
const { slug } = this.recipe.arguments;
const ratios = branches.map(branch => branch.ratio);
// It's important that the input be:
// - Unique per-user (no one is bucketed alike)
// - Unique per-experiment (bucketing differs across multiple experiments)
// - Differs from the input used for sampling the recipe (otherwise only
// branches that contain the same buckets as the recipe sampling will
// receive users)
const input = `${this.normandy.userId}-${slug}-branch`;
const index = await this.normandy.ratioSample(input, ratios);
return branches[index];
}
}
Object(_utils__WEBPACK_IMPORTED_MODULE_0__["registerAction"])('preference-experiment', PreferenceExperimentAction);
/**
* Finds active experiments that were not stored in the seenExperimentNames list
* during action execution, and stop them.
*/
async function postExecutionHook(normandy) {
// Exit early if we're on an incompatible client.
if (normandy.preferenceExperiments === undefined) {
normandy.log('Client does not support preference experiments, aborting.', 'info');
return;
}
// If any of the active experiments were not seen during a run, stop them.
const activeExperiments = await normandy.preferenceExperiments.getAllActive();
for (const experiment of activeExperiments) {
if (!seenExperimentNames.includes(experiment.name)) {
await normandy.preferenceExperiments.stop(experiment.name, {
resetValue: true,
reason: 'recipe-not-seen',
});
}
}
}
Object(_utils__WEBPACK_IMPORTED_MODULE_0__["registerAsyncCallback"])('postExecution', postExecutionHook);
/***/ }),
/***/ "./client/actions/utils.js":
/*!*********************************!*\
!*** ./client/actions/utils.js ***!
\*********************************/
/*! exports provided: Action, registerAction, registerAsyncCallback */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Action", function() { return Action; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "registerAction", function() { return registerAction; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "registerAsyncCallback", function() { return registerAsyncCallback; });
class Action {
constructor(normandy, recipe) {
this.normandy = normandy;
this.recipe = recipe;
}
}
// Attempt to find the global registerAction, and fall back to a noop if it's
// not available.
const registerAction = (
(global && global.registerAction)
|| (window && window.registerAction)
|| function registerAction() {}
);
// Same as above, for registerAsyncCallback
const registerAsyncCallback = (
(global && global.registerAsyncCallback)
|| (window && window.registerAsyncCallback)
|| function registerAsyncCallback() {}
);
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/global.js */ "./node_modules/webpack/buildin/global.js")))
/***/ }),
/***/ "./node_modules/webpack/buildin/global.js":
/*!***********************************!*\
!*** (webpack)/buildin/global.js ***!
\***********************************/
/*! no static exports found */
/***/ (function(module, exports) {
var g;
// This works in non-strict mode
g = (function() {
return this;
})();
try {
// This works if eval is allowed (see CSP)
g = g || new Function("return this")();
} catch (e) {
// This works if the window reference is available
if (typeof window === "object") g = window;
}
// g can still be undefined, but nothing to do about it...
// We return undefined, instead of nothing here, so it's
// easier to handle this case. if(!global) { ...}
module.exports = g;
/***/ })
/******/ });
//# sourceMappingURL=preference-experiment.js.map

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -0,0 +1,568 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./client/actions/show-heartbeat/index.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./client/actions/show-heartbeat/index.js":
/*!************************************************!*\
!*** ./client/actions/show-heartbeat/index.js ***!
\************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return ShowHeartbeatAction; });
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../utils */ "./client/actions/utils.js");
const VERSION = 56; // Increase when changed.
// 24 hours in milliseconds
const ONE_DAY = (1000 * 3600 * 24);
// how much time should elapse between heartbeats?
const HEARTBEAT_THROTTLE = ONE_DAY;
class ShowHeartbeatAction extends _utils__WEBPACK_IMPORTED_MODULE_0__["Action"] {
constructor(normandy, recipe) {
super(normandy, recipe);
// 'local' storage
// (namespaced to recipe.id - only this heartbeat can access)
this.storage = normandy.createStorage(recipe.id);
// 'global' storage
// (constant namespace - all heartbeats can access)
this.heartbeatStorage = normandy.createStorage('normandy-heartbeat');
// context bindings
this.updateLastInteraction = this.updateLastInteraction.bind(this);
this.updateLastShown = this.updateLastShown.bind(this);
}
/**
* Returns a surveyId value. If recipe calls
* to include the Telemetry UUID value,
* then the UUID is attached to the surveyId
* in `<surveyId>::<userId>` format.
*
* @return {String} Survey ID, possibly with user UUID
*/
generateSurveyId() {
const {
includeTelemetryUUID,
surveyId,
} = this.recipe.arguments;
const { userId } = this.normandy;
let value = surveyId;
// should user ID stuff be sent to telemetry?
if (includeTelemetryUUID && !!userId) {
// alter the survey ID to include that UUID
value = `${surveyId}::${userId}`;
}
return value;
}
/**
* Returns a boolean indicating if a heartbeat has been shown recently.
*
* Checks the saved `lastShown` value against the current time
* and returns if the time is under HEARTBEAT_THROTTLE milliseconds.
*
* @async
* @return {Boolean} Has any heartbeat been shown recently?
*/
async heartbeatShownRecently() {
const lastShown = await this.heartbeatStorage.getItem('lastShown');
const timeSince = lastShown
? new Date() - parseFloat(lastShown) : Infinity;
// Return a boolean indicating if a heartbeat
// has shown within the last HEARTBEAT_THROTTLE ms
return timeSince < HEARTBEAT_THROTTLE;
}
/**
* Looks up the time the prompt was last displayed to the user,
* and converts it to a Number (if found).
*
* @async
* @return {number} Timestamp of last prompt showing
*/
async getLastShown() {
const lastShown = await this.storage.getItem('lastShown');
return typeof lastShown !== 'undefined'
? parseFloat(lastShown) : null;
}
/**
* Return whether this survey has been seen by the user before.
* @async
* @return {Boolean}
*/
async hasShownBefore() {
// Even if the stored date is unparsable due to weirdness in the user's
// storage, if there's _something_ stored then we probably have shown at
// least once.
return await this.storage.getItem('lastShown') !== null;
}
/**
* Determines if this heartbeat was shown
* at least x days ago.
*
* @param {Number} days Days ago to check
* @return {boolean} Has prompt been shown by that date?
*/
async shownAtleastDaysAgo(days) {
const hasShown = await this.hasShownBefore();
if (!hasShown) {
return false;
}
// get timestamp of last shown
const timeLastShown = await this.getLastShown();
// get the difference between now and then
const timeElapsed = Date.now() - timeLastShown;
// time limit is the number of days passed in
// converted into milliseconds
const timeLimit = ONE_DAY * days;
// if the diff is smaller than the limit,
// that means that the last time the user saw the prompt
// was less than the `days` passed in
return timeElapsed < timeLimit;
}
/**
* Simple function to read the lastInteraction
* timestamp (if any) from local storage.
* @return {number} Timestamp of last prompt interaction (if any)
*/
async getLastInteraction() {
const lastInteraction = await this.storage.getItem('lastInteraction');
return typeof lastInteraction !== 'undefined'
? parseFloat(lastInteraction) : null;
}
/**
* Gets the timestamp of the last prompt interaction,
* and returns the time (in ms) since then.
*
* @async
* @return {number}
*/
async sinceLastInteraction() {
const lastInteraction = await this.getLastInteraction();
return typeof lastInteraction !== 'undefined'
? Date.now() - lastInteraction : null;
}
/**
* Checks when the survey prompt last had
* interaction from the user (if ever),
* and returns a boolean indicating if the
* user has ever had interaction
*
* @async
* @return {Boolean} Has the survey ever had interaction?
*/
async hasHadInteraction() {
const lastInteraction = await this.getLastInteraction();
return !!lastInteraction;
}
/**
* Checks the repeatOption argument for this recipe
* and determines if the recipe has fully executed.
*
* Each `repeatOption` setting has different requirements
* to consider the heartbeat as executed; `once` will appear to the
* user once and never again, while `nag` may appear to the user multiple times
* before it is interacted with and considers itself 'executed'.
*
* @return {boolean} Has this recipe fulfilled its execution criteria?
*/
async heartbeatHasExecuted() {
let hasShown = false;
const {
repeatOption,
repeatEvery,
} = this.recipe.arguments;
switch (repeatOption) {
// `once` is one and done
default:
case 'once':
hasShown = await this.hasShownBefore();
break;
// `nag` requires user interaction to go away
case 'nag':
hasShown = await this.hasHadInteraction();
break;
// `xdays` waits for `repeatEvery` days to show again
case 'xdays':
hasShown = await this.shownAtleastDaysAgo(repeatEvery);
break;
}
return hasShown;
}
/**
* Returns a boolean if the heartbeat should
* fall out of `execute` or not. Checks
* `testing` mode, and if heartbeats have
* been shown lately.
*
* @return {boolean} Should the recipe execution halt?
*/
async shouldNotExecute() {
return !this.normandy.testing
&& (
// if a heartbeat has been shown in the past 24 hours
await this.heartbeatShownRecently()
// or this specific heartbeat has already ran
|| this.heartbeatHasExecuted()
);
}
/**
* Main action function.
*
* Determines if the heartbeat should be shown,
* and if so, does so. Also records last shown
* times to local storage to track when any
* heartbeat was last shown to the user.
*/
async execute() {
const {
message,
engagementButtonLabel,
thanksMessage,
postAnswerUrl,
learnMoreMessage,
learnMoreUrl,
} = this.recipe.arguments;
// determine if this should even run
if (await this.shouldNotExecute()) {
return;
}
this.client = await this.normandy.client();
// pull some data to attach to the telemetry business
const { userId } = this.normandy;
const surveyId = this.generateSurveyId();
// A bit redundant but the action argument names shouldn't necessarily rely
// on the argument names showHeartbeat takes.
const heartbeatData = {
surveyId,
message,
engagementButtonLabel,
thanksMessage,
learnMoreMessage,
learnMoreUrl,
postAnswerUrl: this.generatePostURL(postAnswerUrl, userId),
// generate a new uuid for this heartbeat flow
flowId: this.normandy.uuid(),
surveyVersion: this.recipe.revision_id,
};
// Add a flag to the heartbeat data if in test mode
if (this.normandy.testing) {
heartbeatData.testing = 1;
}
// show the prompt!
const heartBeat = await this.normandy.showHeartbeat(heartbeatData);
// list of events that the heartBeat will trigger
// based on the user's interaction with the browser chrome
const interactionEvents = ['Voted', 'Engaged'];
// Upon heartbeat interaction, we want to update the stored time
interactionEvents.forEach(event => {
heartBeat.on(event, this.updateLastInteraction);
});
// Let the record show that a heartbeat has been displayed
this.updateLastShown();
}
/**
* Updates the local storage values of when a/this heartbeat
* was last displayed to the user with the current time.
*/
updateLastShown() {
// update the 'personal' storage of this heartbeat
this.storage.setItem('lastShown', Date.now());
// also update the 'global' storage of all heartbeats
this.heartbeatStorage.setItem('lastShown', Date.now());
}
/**
* Updates the local storage value of when this heartbeat
* received an interaction event from
*/
updateLastInteraction() {
this.storage.setItem('lastInteraction', Date.now());
}
/**
* Gathers recipe action/message information, and formats the content into
* URL-safe query params. This is used by generatePostURL to
* inject Google Analytics params into the post-answer URL.
*
* @return {Object} Hash containing utm_ queries to append to post-answer URL
*/
getGAParams() {
let message = this.recipe.arguments.message || '';
// remove spaces
message = message.replace(/\s+/g, '');
// escape what we can
message = encodeURIComponent(message);
// use a fake URL object to get a legit URL-ified URL
const fakeUrl = new URL('http://mozilla.com');
fakeUrl.searchParams.set('message', message);
// pluck the (now encoded) message
message = fakeUrl.search.replace('?message=', '');
return {
utm_source: 'firefox',
utm_medium: this.recipe.action, // action name
utm_campaign: message, // 'shortenedmesssagetext'
};
}
/**
* Given a post-answer url (and optionally a userId), returns an
* updated string with query params of relevant data for the
* page the user will be directed to. Includes survey version,
* google analytics params, etc.
*
* @param {String} url Post-answer URL (without query params)
* @param {String} userId? Optional, UUID to associate with user
* @return {String} URL with post-answer query params
*/
generatePostURL(url, userId) {
// Don't bother with empty URLs.
if (!url) {
return url;
}
const args = {
source: 'heartbeat',
surveyversion: VERSION,
updateChannel: this.client.channel,
fxVersion: this.client.version,
isDefaultBrowser: this.client.isDefaultBrowser ? 1 : 0,
searchEngine: this.client.searchEngine,
syncSetup: this.client.syncSetup ? 1 : 0,
// Google Analytics parameters
...this.getGAParams(),
};
// if a userId is given,
// we'll include it with the data passed through
// to SurveyGizmo (via query params)
if (this.recipe.arguments.includeTelemetryUUID && userId) {
args.userId = userId;
}
// Append testing parameter if in testing mode.
if (this.normandy.testing) {
args.testing = 1;
}
// create a URL object to append arguments to
const annotatedUrl = new URL(url);
for (const key in args) {
if (!args.hasOwnProperty(key)) {
continue;
}
// explicitly set the query param
// (this makes our args URL-safe)
annotatedUrl.searchParams.set(key, args[key]);
}
// return the address with encoded queries
return annotatedUrl.href;
}
}
Object(_utils__WEBPACK_IMPORTED_MODULE_0__["registerAction"])('show-heartbeat', ShowHeartbeatAction);
/***/ }),
/***/ "./client/actions/utils.js":
/*!*********************************!*\
!*** ./client/actions/utils.js ***!
\*********************************/
/*! exports provided: Action, registerAction, registerAsyncCallback */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* WEBPACK VAR INJECTION */(function(global) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Action", function() { return Action; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "registerAction", function() { return registerAction; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "registerAsyncCallback", function() { return registerAsyncCallback; });
class Action {
constructor(normandy, recipe) {
this.normandy = normandy;
this.recipe = recipe;
}
}
// Attempt to find the global registerAction, and fall back to a noop if it's
// not available.
const registerAction = (
(global && global.registerAction)
|| (window && window.registerAction)
|| function registerAction() {}
);
// Same as above, for registerAsyncCallback
const registerAsyncCallback = (
(global && global.registerAsyncCallback)
|| (window && window.registerAsyncCallback)
|| function registerAsyncCallback() {}
);
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/global.js */ "./node_modules/webpack/buildin/global.js")))
/***/ }),
/***/ "./node_modules/webpack/buildin/global.js":
/*!***********************************!*\
!*** (webpack)/buildin/global.js ***!
\***********************************/
/*! no static exports found */
/***/ (function(module, exports) {
var g;
// This works in non-strict mode
g = (function() {
return this;
})();
try {
// This works if eval is allowed (see CSP)
g = g || new Function("return this")();
} catch (e) {
// This works if the window reference is available
if (typeof window === "object") g = window;
}
// g can still be undefined, but nothing to do about it...
// We return undefined, instead of nothing here, so it's
// easier to handle this case. if(!global) { ...}
module.exports = g;
/***/ })
/******/ });
//# sourceMappingURL=show-heartbeat.js.map

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -6,23 +6,11 @@
"type": "git", "type": "git",
"url": "git://github.com/mozilla/normandy.git" "url": "git://github.com/mozilla/normandy.git"
}, },
"scripts": {
"watch": "webpack --config ./webpack.config.js --watch",
"build": "webpack --config ./webpack.config.js",
"lint": "yarn lint:js-security",
"lint:js-security": "yarn audit"
},
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"@mozilla/normandy-action-argument-schemas": "0.10.0" "@mozilla/normandy-action-argument-schemas": "0.10.0"
}, },
"devDependencies": { "devDependencies": {
"gh-pages": "2.2.0", "gh-pages": "2.2.0"
"webpack": "4.43.0",
"webpack-cli": "3.3.11"
},
"resolutions": {
"set-value": ">=2.0.1 <3.0.0 || >=3.0.1",
"minimist": ">=1.2.3"
} }
} }

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

@ -1,72 +0,0 @@
var path = require('path');
var webpack = require('webpack');
var childProcess = require('child_process');
const BOLD = '\u001b[1m';
const END_BOLD = '\u001b[39m\u001b[22m';
const production = process.env.NODE_ENV === 'production';
const jsNamePattern = '[name].js';
var plugins = [
new webpack.DefinePlugin({
PRODUCTION: production,
DEVELOPMENT: !production,
process: {
env: {
NODE_ENV: production ? '"production"' : '"development"',
},
},
}),
];
if (!production) {
plugins = plugins.concat([
new webpack.NoEmitOnErrorsPlugin(),
]);
}
module.exports = function (webpackEnvOptions) {
var envOptions = webpackEnvOptions || {
'update-actions': false,
};
return {
devtool: production ? undefined : 'cheap-module-source-map',
mode: production ? 'production' : 'development',
entry: {
'console-log': './client/actions/console-log/index',
'show-heartbeat': './client/actions/show-heartbeat/index',
'preference-experiment': './client/actions/preference-experiment/index',
'opt-out-study': './client/actions/opt-out-study/index',
},
plugins: plugins.concat([
// Small plugin to update the actions in the database if
// --env.update-actions was passed.
function updateActions() {
this.plugin('done', function () {
var cmd;
if (envOptions['update-actions']) {
// Don't disable actions since this is mostly for development.
cmd = 'python manage.py update_actions';
childProcess.exec(cmd, function (err, stdout, stderr) {
console.log('\n' + BOLD + 'Updating Actions' + END_BOLD);
console.log(stdout);
if (stderr) {
console.error(stderr);
}
});
}
});
},
]),
output: {
path: path.resolve('./assets/bundles/'),
filename: jsNamePattern,
},
};
};

2979
yarn.lock

Разница между файлами не показана из-за своего большого размера Загрузить разницу