diff --git a/config.example.js b/config.example.js index 31e371b..f7a3ca1 100644 --- a/config.example.js +++ b/config.example.js @@ -8,5 +8,17 @@ module.exports = { // S3 Region s3Region: 'ap-southeast-2', // allow Mojito load more than one times - allowMultiInstance: false + allowMultiInstance: false, + // Lifecycle Events conf + lifecycleEvents: { + ci: { + branch: 'your-production-branch' + }, + analytics: { + snowplow: { + appId: 'your-app-id', + collectorUrl: 'your.collector.com' + } + } + } }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 00107d4..ab88a04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mojito-js-delivery", - "version": "2.4.2", + "version": "2.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -771,6 +771,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true + }, "@smithy/abort-controller": { "version": "2.0.15", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.0.15.tgz", @@ -1335,15 +1341,89 @@ "tslib": "^2.5.0" } }, + "@snowplow/node-tracker": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@snowplow/node-tracker/-/node-tracker-3.23.0.tgz", + "integrity": "sha512-ZeSXrsMnUxJJxrgHR2aSze8V6uD7OCF/Han9DxAklcEl3e4HIeYzVFTYgcjAQg9JKHUqPGJBGHhenp3+0Ml0/Q==", + "dev": true, + "requires": { + "@snowplow/tracker-core": "3.23.0", + "got": "^11.8.5", + "tslib": "^2.3.1" + } + }, + "@snowplow/tracker-core": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@snowplow/tracker-core/-/tracker-core-3.23.0.tgz", + "integrity": "sha512-q6Z7czCjyfzhnftnofWo+jaLf3jdmTO8yNze7WbJcnZBTQjO0iMjx1ffVieZ38zFOc+O6ANLv4roMfmgyohwiQ==", + "dev": true, + "requires": { + "tslib": "^2.3.1", + "uuid": "^3.4.0" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true + }, + "@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { - "version": "20.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", - "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", + "version": "20.12.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz", + "integrity": "sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==", "dev": true, "requires": { "undici-types": "~5.26.4" } }, + "@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -1505,6 +1585,27 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true + }, + "cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + } + }, "camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -1595,6 +1696,15 @@ "wrap-ansi": "^7.0.0" } }, + "clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1652,6 +1762,29 @@ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + } + } + }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true + }, "devtools-protocol": { "version": "0.0.1045489", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1045489.tgz", @@ -1831,6 +1964,25 @@ "is-glob": "^4.0.1" } }, + "got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1843,6 +1995,22 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -1964,6 +2132,21 @@ "argparse": "^2.0.1" } }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, "lighthouse-logger": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", @@ -1993,12 +2176,24 @@ "is-unicode-supported": "^0.1.0" } }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + }, "marky": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", "dev": true }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true + }, "minimatch": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", @@ -2106,6 +2301,12 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2115,6 +2316,12 @@ "wrappy": "1" } }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true + }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -2208,6 +2415,12 @@ } } }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -2243,6 +2456,21 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, + "resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, + "responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "requires": { + "lowercase-keys": "^2.0.0" + } + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", diff --git a/package.json b/package.json index 1923724..1653363 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mojito-js-delivery", - "version": "2.5.0", + "version": "2.6.0", "description": "Utilities to build, launch and measure split tests. Part of the Mojito framework.", "scripts": { "build": "node scripts/command.js --command build", @@ -9,10 +9,11 @@ "deploy": "node scripts/command.js --command deploy", "new": "node scripts/command.js --command new", "set": "node scripts/command.js --command set", - "watch": "nodemon --on-change-only -e js,css,yml --watch ./lib --exec npm run deploy" + "watch": "nodemon --on-change-only -e js,css,yml --watch ./lib --exec npm run deploy", + "getChangedFiles": "node scripts/cli-get-changed-files.js" }, "devDependencies": { - "chrome-launcher": "^0.15.0", + "chrome-launcher": "^0.15.2", "clean-css": "^5.1.2", "mocha": "^10.2.0", "puppeteer-core": "^18.0.5", @@ -20,7 +21,8 @@ "js-yaml": "^4.1.0", "terser": "^5.26.0", "@aws-sdk/client-s3": "^3.478.0", - "cli-table": "^0.3.11" + "cli-table": "^0.3.11", + "@snowplow/node-tracker": "^3.22.1" }, "dependencies": {} } diff --git a/scripts/cli-build.js b/scripts/cli-build.js index 541d471..021a651 100644 --- a/scripts/cli-build.js +++ b/scripts/cli-build.js @@ -6,8 +6,12 @@ const zlib = require('zlib'); const { minify } = require('terser'); const cliTable = require('cli-table'); const config = require('../config'); +const sendLifecycleEventsFn = require('./cli-send-lifecycle-events'); -let builtWaves = {}; +let args, + builtWaves = {}, + changedConfigFiles = {}, + comparingCommits; /** * build a test object * @param {String} configFile, config file path @@ -20,7 +24,10 @@ function buildTest(configFile, buildResult) { dirname; dirname = path.dirname(configFile); - testObject = tokenizePaths(yaml.load(fs.readFileSync(configFile, 'utf8'))); + testObject = yaml.load(fs.readFileSync(configFile, 'utf8')); + sendLifecycleEvents(testObject, configFile); + + testObject = tokenizePaths(testObject); // skip inactive tests if (testObject.state == 'inactive') { @@ -295,10 +302,38 @@ function appendBuildInfo(containerData, content) { return content + '\r' + buildInfo; } +function sendLifecycleEvents(testObject, configFile) { + if (!args.trackLifecycleEvents || !(configFile in changedConfigFiles)) { + return; + } + + sendLifecycleEventsFn(args, { + testObject: testObject, + configFilePath: changedConfigFiles[configFile], + comparingCommits: comparingCommits + }); +} + +function parseChangedFileList(rootPath) { + let changedFilePath = '/etc/mojito-changed-files.txt'; + let changedFleList = fs.readFileSync(changedFilePath, 'utf8').split('\n'); + + let fileName; + for (let i=0,c=changedFleList.length;i> /etc/mojito-changed-files.txt'); + fs.writeFileSync('/etc/mojito-comparing-commits.txt', lastCommitHash + '...' + currentCommitHash); + } else { + execSync('git log --name-only --format="" ' + currentCommitHash + ' >> /etc/mojito-changed-files.txt'); + fs.writeFileSync('/etc/mojito-comparing-commits.txt', currentCommitHash); + } +} + +async function getLastBuildTime(args) { + let branch = 'master'; + if (config.lifecycleEvents.ci && config.lifecycleEvents.ci.branch) { + branch = config.lifecycleEvents.ci.branch; + } + + let url = `https://api.bitbucket.org/2.0/repositories/${args.workspace}/${args.repoSlug}/pipelines/?status=PASSED&status=SUCCESSFUL&target.ref_name=${branch}&sort=-created_on`; + let res = await fetch(url, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${args.repoAccessToken}` + } + }); + + if (res.ok) { + let data = await res.json(); + if (data.values.length) { + let lastPipeline = data.values[0]; + runGitLog(lastPipeline.target.commit.hash, args.commitHash); + } else { + runGitLog(null, args.commitHash); + } + } else { + throw new Error(`Mojito Building - get pipeline data failed.`); + } +} + +module.exports = async function getChangedFiles(args) { + getLastBuildTime(args); +}; \ No newline at end of file diff --git a/scripts/cli-get-changed-files.js b/scripts/cli-get-changed-files.js new file mode 100644 index 0000000..a2efedf --- /dev/null +++ b/scripts/cli-get-changed-files.js @@ -0,0 +1,92 @@ +const config = require('../config'); +const getChangedFilesBitbucketPipelines = require('./cli-get-changed-files-bitbucket-pipelines'); + +const supportedCIs = ['bitbucket-pipelines']; +const supportedAnalytics = ['snowplow']; + +let getChangedFilesFn; + +function parseCLIArgs(defaultNull) { + let validTestStates = { + inactive: 1, + staging: 1, + live: 1, + divert: 1 + }; + + let argList = process.argv; + let args = {}, i, opt, thisOpt, curOpt; + for (i = 0; i < argList.length; i++) { + thisOpt = argList[i].trim(); + opt = thisOpt.replace(/^\-+/, ''); + + if (opt === thisOpt) { + // argument value + if (curOpt) { + args[curOpt] = opt; + } else { + if (defaultNull && validTestStates[opt]) { + args['state'] = opt; + } + } + + curOpt = null; + } + else { + // argument name + curOpt = opt; + args[curOpt] = defaultNull ? null : true; + } + } + + if (defaultNull) { + delete args.command; + } + + return args; +} + +function checkConfig() { + let eventsConf = config.lifecycleEvents; + if (!eventsConf) { + throw new Error(`Mojito Lifecycle events - please specify lifecycleEvents configuration in config.js.`); + } + + let ciEnv = 'bitbucket-pipelines'; + if (eventsConf.ci && eventsConf.ci.environment) { + ciEnv = eventsConf.ci.environment; + } + + if (!supportedCIs.includes(ciEnv)) { + throw new Error(`Mojito Lifecycle events - unspported CI environment: ${ciEnv}.`); + } + + // todo: support more CI environments + if (ciEnv == 'bitbucket-pipelines') { + getChangedFilesFn = getChangedFilesBitbucketPipelines; + } + + let analyticsConf = eventsConf.analytics||{}; + if (Object.keys(analyticsConf) <= 0) { + throw new Error(`Mojito Lifecycle events - please specify analytics configuration in config.js.`); + } + + let analytics = Object.keys(analyticsConf)[0]; + if (!supportedAnalytics.includes(analytics)) { + throw new Error(`Mojito Lifecycle events - unspported analytics: ${analytics}.`); + } + + if (analytics == 'snowplow') { + if (!analyticsConf.snowplow.collectorUrl) { + throw new Error(`Mojito Lifecycle events - please specify snowplow collectorUrl in config.js.`); + } + } +} + +async function getChangedFiles() { + checkConfig(); + let args = parseCLIArgs(); + getChangedFilesFn(args); +} + +getChangedFiles(); diff --git a/scripts/cli-send-lifecycle-events-snowplow.js b/scripts/cli-send-lifecycle-events-snowplow.js new file mode 100644 index 0000000..69d933e --- /dev/null +++ b/scripts/cli-send-lifecycle-events-snowplow.js @@ -0,0 +1,37 @@ +const config = require('../config'); +const snowplow = require('@snowplow/node-tracker'); + +module.exports = function sendLifecycleEvents(args, payload) { + console.log('sending lifecycle event to snowplow...'); + let configData = JSON.parse(JSON.stringify(payload.testObject)); + + let snowplowConf = config.lifecycleEvents.analytics.snowplow; + let emitter = snowplow.gotEmitter( + snowplowConf.collectorUrl, + snowplowConf.collectorProtocol||snowplow.HttpProtocol.HTTPS, + snowplowConf.collectorPort||443, + snowplowConf.collectorPayload||snowplow.HttpMethod.GET, + 1 + ); + + let tracker = snowplow.tracker([emitter], 'mojito-js-delivery', snowplowConf.appId); + tracker.track(snowplow.buildStructEvent({ + category: 'mojito wave lifecycle event', + action: payload.configFilePath, + label: payload.comparingCommits + }), + [ + { + schema: 'iglu:io.mintmetrics.mojito/mojito_wave_configuration/jsonschema/1-0-0', + data: configData + }, + { + schema: 'iglu:io.mintmetrics.mojito/mojito_container_metadata/jsonschema/1-0-0', + data: { + name: config.containerName, + version: process.env.npm_package_version, + commit: args.commitHash + } + } + ]); +} \ No newline at end of file diff --git a/scripts/cli-send-lifecycle-events.js b/scripts/cli-send-lifecycle-events.js new file mode 100644 index 0000000..c428f02 --- /dev/null +++ b/scripts/cli-send-lifecycle-events.js @@ -0,0 +1,15 @@ +const config = require('../config'); +const sendLifecycleEventsSnowplow = require('./cli-send-lifecycle-events-snowplow'); + +let sendLifecycleEventsFn; +let analytics; +if (config.lifecycleEvents && config.lifecycleEvents.analytics) { + analytics = Object.keys(config.lifecycleEvents.analytics)[0]; +} + +// todo: support more analytics +if (analytics == 'snowplow') { + sendLifecycleEventsFn = sendLifecycleEventsSnowplow; +} + +module.exports = sendLifecycleEventsFn; \ No newline at end of file diff --git a/scripts/command.js b/scripts/command.js index f6f02dd..9f8054b 100644 --- a/scripts/command.js +++ b/scripts/command.js @@ -78,7 +78,7 @@ async function runCommand() { switch (command) { case 'build': - mojitoBuild(); + mojitoBuild(args); break; case 'test': runTests(); diff --git a/wave.schema.json b/wave.schema.json index bdf5725..b5c4b13 100644 --- a/wave.schema.json +++ b/wave.schema.json @@ -1,22 +1,27 @@ { - "$id": "https://mojito.mintmetrics.io/wave.schema.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Wave configuration 1-0-0", + "$schema": "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#", + "title": "Wave configuration", "description": "A Mojito Wave configuration for generating a test object", + "self": { + "vendor": "io.mintmetrics.mojito", + "name": "mojito_wave_configuration", + "format": "jsonschema", + "version": "1-0-0" + }, "type": "object", "properties": { "id": { "title": "Wave ID", - "description": "The test ID and part of the experiment salt", + "description": "The test ID. Also used as part of the experiment salt.", "type": "string", "pattern": "^[a-zA-Z0-9-]+$", - "maxLength": 36 + "maxLength": 128 }, "name": { "title": "Wave name", - "description": "The name of the test used in reports", + "description": "The name of the test to be used in reports", "type": "string", - "maxLength": 128 + "maxLength": 255 }, "sampleRate": { "title": "Sample rate", @@ -26,11 +31,11 @@ "maximum": 1 }, "divertTo": { - "title": "Divert to recipe", + "title": "Divert to a recipe", "description": "Divert all traffic in this test to a specified recipe ID", "type": "string", "pattern": "^[a-zA-Z0-9]+$", - "maxLength": 36 + "maxLength": 128 }, "recipes": { "title": "Recipes object", @@ -53,13 +58,15 @@ "title": "Recipe JS", "description": "Relative path to the recipe's JS function", "type": "string", - "pattern": "\\.js$" + "pattern": "\\.js$", + "maxLength": 128 }, "css": { "title": "Recipe CSS", "description": "Relative path to the recipe's CSS stylesheet", "type": "string", - "pattern": "\\.css$" + "pattern": "\\.css$", + "maxLength": 128 }, "sampleRate": { "title": "Sample rate", @@ -69,7 +76,7 @@ "maximum": 1 } }, - "additionalProperties": false, + "additionalProperties": true, "required": ["name"] } }, @@ -79,24 +86,28 @@ "title": "Trigger function", "description": "Relative path to the experiment's trigger JS function - the function that activates an experiment", "type": "string", - "pattern": "\\.js$" + "pattern": "\\.js$", + "maxLength": 128 }, "js": { "title": "Shared JS", "description": "Relative path to a shared JS function run before all recipes", "type": "string", - "pattern": "\\.js$" + "pattern": "\\.js$", + "maxLength": 128 }, "css": { "title": "Shared CSS", "description": "Relative path to shared CSS styles applied for all recipes", "type": "string", - "pattern": "\\.css$" + "pattern": "\\.css$", + "maxLength": 128 }, "gaExperimentId": { "title": "Google Optimize Experiment ID", "description": "Track this experiment in Google Optimize under a given Experiment ID", - "type": "string" + "type": "string", + "maxLength": 40 }, "state": { "title": "State", @@ -110,7 +121,7 @@ "type": "boolean" } }, - "required": ["id", "name", "recipes", "state", "sampleRate", "trigger"], + "required": ["id", "name", "recipes", "state", "sampleRate"], "additionalProperties": true, "oneOf": [{ "properties": {