зеркало из 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
|
||||
.DS_Store
|
||||
__pycache__/
|
||||
/assets/
|
||||
/static/
|
||||
/media/
|
||||
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"
|
||||
exclude: "docs/"
|
||||
|
||||
yarn-audit:
|
||||
run: yarn audit
|
||||
|
||||
missing-migirations:
|
||||
include: "*.py"
|
||||
run: |
|
||||
|
|
|
@ -33,8 +33,7 @@ COPY ./poetry.lock /app/poetry.lock
|
|||
RUN poetry install --no-dev --no-root --no-interaction --verbose
|
||||
|
||||
COPY . /app
|
||||
RUN NODE_ENV=production yarn build && \
|
||||
DJANGO_CONFIGURATION=Build python ./manage.py collectstatic --no-input && \
|
||||
RUN DJANGO_CONFIGURATION=Build python ./manage.py collectstatic --no-input && \
|
||||
mkdir -p media && chown app:app media
|
||||
|
||||
USER app
|
||||
|
|
|
@ -34,8 +34,7 @@ RUN poetry install --no-root --extras docs
|
|||
|
||||
COPY . /app
|
||||
|
||||
RUN NODE_ENV=production yarn build && \
|
||||
DJANGO_CONFIGURATION=Build python ./manage.py collectstatic --no-input && \
|
||||
RUN DJANGO_CONFIGURATION=Build python ./manage.py collectstatic --no-input && \
|
||||
mkdir -p media
|
||||
|
||||
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",
|
||||
"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",
|
||||
"dependencies": {
|
||||
"@mozilla/normandy-action-argument-schemas": "0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"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"
|
||||
"gh-pages": "2.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Загрузка…
Ссылка в новой задаче