Skip to content

Commit

Permalink
feat(cli): add opt-out analytics to diez (#92)
Browse files Browse the repository at this point in the history
 - Add a `serverless` back end for pinging Amplitude with `diez` CLI activity events.
 - Activate opt-out analytics every time the `diez` CLI is bootstrapped.
 - Add a command, `diez analytics <on|off>`, for easily enabling/disabling analytics.
  • Loading branch information
stristr committed May 17, 2019
1 parent 7692103 commit 08291ec
Show file tree
Hide file tree
Showing 44 changed files with 610 additions and 59 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1 +1 @@
TODO
Refer to Beta Software License Agreement
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"prepack": "yarn diez minify-sources"
},
"resolutions": {
"istanbul-api": "^2.0.0"
"istanbul-api": "^2.0.0",
"typescript": "^3.4.5"
}
}
18 changes: 18 additions & 0 deletions packages/analytics/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Serverless directories
.serverless

# golang output binary directory
bin

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
11 changes: 11 additions & 0 deletions packages/analytics/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
build:
env GOOS=linux go build -ldflags="-s -w" -o bin/ping ping/main.go

clean:
rm -rf ./bin

deploy-dev: clean build
sls deploy --verbose --aws-profile diez-serverless --stage dev

deploy: clean build
sls deploy --verbose --aws-profile diez-serverless --stage prod
25 changes: 25 additions & 0 deletions packages/analytics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# `@diez/analytics`

A simple analytics back end for Diez built with [`serverless`](https://serverless.com).

This function provides a simple anonymizing relay to Amplitude devoid of any PII.

The following environment is required in order to deploy the analytics back end to AWS:
- Environment variables:
- `DIEZ_HOSTED_ZONE_ID`: a Route 53 Hosted Zone ID for `diez.org`.
- `DIEZ_AMPLITUDE_API_KEY_DEV`: an Amplitude API key for the "dev" stage.
- `DIEZ_AMPLITUDE_API_KEY_PROD`: an Amplitude API key for the "prod" stage.
- AWS profiles:
- `diez-serverless`: a profile with the requisite permissions for `serverless` ([see gist](https://gist.github.com/ServerlessBot/7618156b8671840a539f405dea2704c8)) and `serverless-domain-manager` ([see "Prerequisities"](https://github.com/amplify-education/serverless-domain-manager#prerequisites)).

First-time activation of domains requires running `create_domain` from `serverless-domain-manager`, e.g.:

```
$ sls create_domain --aws-profile diez-serverless --stage dev
```

### Deploying

- Run `make deploy-dev` to deploy the dev stage.
- Run `make deploy` to deploy the prod stage.
- See [`Makefile`](./Makefile) for details.
8 changes: 8 additions & 0 deletions packages/analytics/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/diez/analytics

require (
github.com/aws/aws-lambda-go v1.10.0
github.com/savaki/amplitude-go v0.0.0-20160610055645-f62e3b57c0e4
github.com/stretchr/testify v1.3.0 // indirect
golang.org/x/net v0.0.0-20190514140710-3ec191127204 // indirect
)
16 changes: 16 additions & 0 deletions packages/analytics/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
github.com/aws/aws-lambda-go v1.10.0 h1:uafgdfYGQD0UeT7d2uKdyWW8j/ZYRifRPIdmeqLzLCk=
github.com/aws/aws-lambda-go v1.10.0/go.mod h1:zUsUQhAUjYzR8AuduJPCfhBuKWUaDbQiPOG+ouzmE1A=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/savaki/amplitude-go v0.0.0-20160610055645-f62e3b57c0e4 h1:97Sfylvx7jxNwMDa6m3wRYQ0sCwP8R7ZGneF57jTSP0=
github.com/savaki/amplitude-go v0.0.0-20160610055645-f62e3b57c0e4/go.mod h1:+dADDNKPvI1c6TuMfh+UakKF37l2i4ok/+wpEVT2KII=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190514140710-3ec191127204 h1:4yG6GqBtw9C+UrLp6s2wtSniayy/Vd/3F7ffLE427XI=
golang.org/x/net v0.0.0-20190514140710-3ec191127204/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
11 changes: 11 additions & 0 deletions packages/analytics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@diez/analytics",
"private": true,
"version": "10.0.0-alpha.0",
"description": "An analytics back end for the Diez CLI.",
"author": "The Haiku Team <contact@haiku.ai>",
"license": "Proprietary",
"devDependencies": {
"serverless-domain-manager": "^3.2.2"
}
}
44 changes: 44 additions & 0 deletions packages/analytics/ping/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/savaki/amplitude-go"

"encoding/json"
"os"
)

type response events.APIGatewayProxyResponse

type ping struct {
UUID string `json:"uuid"`
EventType string `json:"eventType"`
Properties map[string]interface{} `json:"properties"`
}

func handler(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
event := new(ping)
err := json.Unmarshal([]byte(req.Body), event)
if err != nil {
return events.APIGatewayProxyResponse{
StatusCode: 400,
}, err
}

client := amplitude.New(os.Getenv("AMPLITUDE_API_KEY"))
client.Publish(amplitude.Event{
DeviceId: event.UUID,
EventType: event.EventType,
EventProperties: event.Properties,
})
client.Flush()
client.Close()
return events.APIGatewayProxyResponse{
StatusCode: 204,
}, nil
}

func main() {
lambda.Start(handler)
}
35 changes: 35 additions & 0 deletions packages/analytics/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
service: analytics
frameworkVersion: ">=1.28.0 <2.0.0"
plugins:
- serverless-domain-manager
custom:
domainNames:
dev: analytics-dev.diez.org
prod: analytics.diez.org
amplitudeApiKeys:
dev: ${env:DIEZ_AMPLITUDE_API_KEY_DEV}
prod: ${env:DIEZ_AMPLITUDE_API_KEY_PROD}
customDomain:
hostedZoneId: ${env:DIEZ_HOSTED_ZONE_ID}
domainName: ${self:custom.domainNames.${opt:stage}}
certificateName: diez.org
provider:
name: aws
runtime: go1.x
stage: ${opt:stage, "dev"}
region: us-east-1
environment:
AMPLITUDE_API_KEY: ${self:custom.amplitudeApiKeys.${opt:stage}}
package:
exclude:
- ./**
include:
- ./bin/**

functions:
ping:
handler: bin/ping
events:
- http:
path: ping
method: post
7 changes: 7 additions & 0 deletions packages/cli-core/.diezrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"providers": {
"commands": [
"./lib/commands/analytics"
]
}
}
2 changes: 0 additions & 2 deletions packages/cli-core/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,5 @@ module.exports = {
],
moduleNameMapper: {
'^starting-point$': '<rootDir>/test/fixtures/starting-point',
'^command-provider$': '<rootDir>/test/fixtures/command-provider',
'^command-extender$': '<rootDir>/test/fixtures/command-extender',
},
};
4 changes: 2 additions & 2 deletions packages/cli-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@
"typescript": "^3.4.5"
},
"dependencies": {
"@diez/storage": "^10.0.0-alpha.0",
"async": "^2.6.2",
"chalk": "^2.4.2",
"commander": "^2.19.0",
"enquirer": "^2.3.0",
"fs-extra": "^7.0.1",
"package-json": "^6.3.0",
"semver": "^6.0.0",
"server-destroy": "^1.0.1",
"stack-trace": "^0.0.10",
"typed-errors": "^1.1.0"
}
}
32 changes: 31 additions & 1 deletion packages/cli-core/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* tslint:disable no-var-requires */
import {emitDiagnostics, enableAnalytics, Registry} from '@diez/storage';
import chalk from 'chalk';
import {args, command, help, on, parse, version} from 'commander';
import packageJson from 'package-json';
import semver from 'semver';
Expand All @@ -10,11 +12,20 @@ import {
CliOptionValidator,
ValidatedCommand,
} from './api';
import {fatalError, warning} from './reporting';
import {fatalError, info, warning} from './reporting';
import {cliRequire, diezVersion, findPlugins} from './utils';

version(diezVersion).name('diez');

declare global {
namespace NodeJS {
interface Global {
// Captured once at startup in case the value is mutated after starting up.
doNotTrack: boolean;
}
}
}

/**
* Registers a list of options with a command.
* @internal
Expand Down Expand Up @@ -159,6 +170,25 @@ export const bootstrap = async (rootPackageName = global.process.cwd(), bootstra
* @ignore
*/
export const run = async (bootstrapRoot?: string) => {
const analyticsEnabled = await Registry.get('analyticsEnabled');
global.doNotTrack = !analyticsEnabled;
if (analyticsEnabled === undefined) {
console.log(chalk.underline('Anonymous aggregate analytics:'));
info(`
Diez collects diagnostic and usage data each time you use the CLI using an
anonymous, randomly generated ID. We use these data to help improve our
services.
By default, anonymous aggregate analytics will be activated the next time you
run a Diez command. Learn more about what data we collect and how to opt out
here: https://diez.org/analytics`);
await enableAnalytics();
}

if (!global.doNotTrack) {
emitDiagnostics('activity', diezVersion);
}

await bootstrap(global.process.cwd(), bootstrapRoot);
// istanbul ignore next
on('command:*', () => {
Expand Down
24 changes: 24 additions & 0 deletions packages/cli-core/src/commands/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {disableAnalytics, enableAnalytics} from '@diez/storage';
import {CliCommandProvider} from '../api';
import {info} from '../reporting';

const provider: CliCommandProvider = {
name: 'analytics <on|off>',
description: 'Turn Diez analytics on or off.',
action: async (_, toggle: string) => {
switch (toggle) {
case 'on':
await enableAnalytics();
info('Diez analytics are enabled. Learn more here: https://diez.org/analytics');
break;
case 'off':
await disableAnalytics();
info('Diez analytics are disabled. Learn more here: https://diez.org/analytics');
break;
default:
throw new Error(`Unknown state: "${toggle}". Please specify either "on" or "off".`);
}
},
};

export = provider;
24 changes: 12 additions & 12 deletions packages/cli-core/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {each} from 'async';
import {exec as coreExec, ExecException, ExecOptions} from 'child_process';
import {existsSync, readJsonSync} from 'fs-extra';
import {existsSync, readFileSync} from 'fs';
import {platform} from 'os';
import {AbbreviatedVersion as PackageJson} from 'package-json';
import {dirname, join} from 'path';
import {DiezConfiguration} from './api';
import {warning} from './reporting';

// tslint:disable-next-line:no-var-requires
const packageJson = require(join('..', 'package.json'));
Expand Down Expand Up @@ -86,11 +87,7 @@ const getDependencies = (
}

const packagePath = dirname(packageJsonPath);
const json = readJsonSync(packageJsonPath, {throws: false});
// istanbul ignore if
if (!json) {
return;
}
const json = require(packageJsonPath);

foundPackages.set(isRootPackage ? '.' : packageName, {json, path: packagePath});

Expand Down Expand Up @@ -135,12 +132,15 @@ export const findPlugins = (
([packageName, {json, path}], next) => {
const configuration = (json.diez || {}) as DiezConfiguration;
const diezRcPath = join(path, '.diezrc');
if (existsSync(diezRcPath)) {
// TODO: support alternative formats (e.g. YAML) here.
const rcConfiguration = readJsonSync(diezRcPath, {throws: false});
if (rcConfiguration) {
Object.assign(configuration, rcConfiguration);
}
if (!existsSync(diezRcPath)) {
return next();
}

try {
const rcConfiguration = JSON.parse(readFileSync(diezRcPath).toString());
Object.assign(configuration, rcConfiguration);
} catch (error) {
warning(`Found invalid .diezrc at ${diezRcPath}`);
}

if (Object.keys(configuration).length) {
Expand Down
45 changes: 45 additions & 0 deletions packages/cli-core/test/cli.analytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {assignMock} from '@diez/test-utils';

const mockEmitDiagnostics = jest.fn();
const mockEnableAnalytics = jest.fn();
const mockDisableAnalytics = jest.fn();
jest.doMock('@diez/storage', () => ({
Registry: {
get () {
return true;
},
},
emitDiagnostics: mockEmitDiagnostics,
enableAnalytics: mockEnableAnalytics,
disableAnalytics: mockDisableAnalytics,
}));

import {run} from '../src/cli';
import {action as analyticsAction} from '../src/commands/analytics';
import {diezVersion} from '../src/utils';

beforeEach(() => {
assignMock(process, 'exit');
});

jest.mock('package-json');

describe('cli.analytics', () => {
test('analytics ping', async () => {
await run();
expect(mockEmitDiagnostics).toHaveBeenCalledTimes(1);
expect(mockEmitDiagnostics).toHaveBeenCalledWith('activity', diezVersion);
});

test('analytics <on|off> command', async () => {
await analyticsAction({}, 'on');
expect(mockEnableAnalytics).toHaveBeenCalledTimes(1);
expect(mockDisableAnalytics).toHaveBeenCalledTimes(0);
await analyticsAction({}, 'off');
expect(mockEnableAnalytics).toHaveBeenCalledTimes(1);
expect(mockDisableAnalytics).toHaveBeenCalledTimes(1);
await expect(analyticsAction({}, 'medium')).rejects.toThrow();
expect(mockEnableAnalytics).toHaveBeenCalledTimes(1);
expect(mockDisableAnalytics).toHaveBeenCalledTimes(1);
});
});
Loading

0 comments on commit 08291ec

Please sign in to comment.