зеркало из https://github.com/mozilla/normandy.git
Remove JS build step and tooling
This commit is contained in:
Родитель
af64ac2951
Коммит
cd59cf6b8b
43
.eslintrc
43
.eslintrc
|
@ -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]
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
31
.stylelintrc
31
.stylelintrc
|
@ -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
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
14
package.json
14
package.json
|
@ -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
2979
yarn.lock
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Загрузка…
Ссылка в новой задаче