Skip to content

Commit

Permalink
Implement metrics command (google#353)
Browse files Browse the repository at this point in the history
  • Loading branch information
kalmi committed Mar 23, 2020
1 parent 70492e8 commit 773cb54
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 0 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,14 @@ Lists your most recent Apps Script projects.

- `clasp list`: Prints `helloworld1 – xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...`

### Metrics

Show metrics for a script.

#### Examples

- `clasp metrics`

## Advanced Commands

> **NOTE**: These commands require Project ID/credentials setup (see below).
Expand Down
2 changes: 2 additions & 0 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export async function authorize(options: {
'https://www.googleapis.com/auth/script.deployments', // Apps Script deployments
'https://www.googleapis.com/auth/script.projects', // Apps Script management
'https://www.googleapis.com/auth/script.webapp.deploy', // Apps Script Web Apps
'https://www.googleapis.com/auth/script.metrics', // Apps Script Metrics
'https://www.googleapis.com/auth/drive.metadata.readonly', // Drive metadata
'https://www.googleapis.com/auth/drive.file', // Create Drive files
'https://www.googleapis.com/auth/service.management', // Cloud Project Service Management API
Expand All @@ -129,6 +130,7 @@ export async function authorize(options: {
'https://www.googleapis.com/auth/script.deployments', // Apps Script deployments
'https://www.googleapis.com/auth/script.projects', // Apps Script management
'https://www.googleapis.com/auth/script.webapp.deploy', // Apps Script Web Apps
'https://www.googleapis.com/auth/script.metrics', // Apps Script Metrics
'https://www.googleapis.com/auth/drive.metadata.readonly', // Drive metadata
'https://www.googleapis.com/auth/drive.file', // Create Drive files
'https://www.googleapis.com/auth/service.management', // Cloud Project Service Management API
Expand Down
1 change: 1 addition & 0 deletions src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export default async (options: { localhost?: boolean; creds?: string; status?: b
'https://www.googleapis.com/auth/script.deployments', // Apps Script deployments
'https://www.googleapis.com/auth/script.projects', // Apps Script management
'https://www.googleapis.com/auth/script.webapp.deploy', // Apps Script Web Apps
'https://www.googleapis.com/auth/script.metrics', // Apps Script Metrics
'https://www.googleapis.com/auth/drive.metadata.readonly', // Drive metadata
'https://www.googleapis.com/auth/drive.file', // Create Drive files
'https://www.googleapis.com/auth/service.management', // Cloud Project Service Management API
Expand Down
87 changes: 87 additions & 0 deletions src/commands/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { loadAPICredentials, script } from '../auth';
import { checkIfOnline, getProjectSettings, LOG, logError, spinner, ERROR } from '../utils';
import { script_v1 } from 'googleapis';

/**
* Displays metrics for the current script
* @param cmd.json {boolean} Displays the status in json format.
*/
export default async (): Promise<void> => {
await checkIfOnline();
await loadAPICredentials();
const { scriptId } = await getProjectSettings();
if (!scriptId) return;
spinner.setSpinnerTitle(LOG.METRICS(scriptId)).start();
const metrics = await script.projects.getMetrics({
scriptId,
metricsGranularity: 'DAILY',
});
if (spinner.isSpinning()) spinner.stop(true);
if (metrics.status !== 200) logError(metrics.statusText);
const { data } = metrics;

type Maybe<T> = T | undefined | null;
// Function to format a time range into a user friendly format.
// API appears to always returns whole UTC days. Bail out if this assumption doesn't hold.
const formatTime = ({startTime, endTime}: {startTime: Maybe<string>, endTime: Maybe<string>}) =>
(startTime?.endsWith('T00:00:00Z') && endTime?.endsWith('T00:00:00Z')) ?
startTime.slice(0, 10) :
logError(ERROR.METRICS_UNEXPECTED_RANGE);

// Function to create a Map from an array of MetricsValues (time range -> value)
const array2map = (metricsValues: script_v1.Schema$MetricsValue[]) =>
new Map(metricsValues.map(({startTime, endTime, value}) => ([
formatTime({ startTime , endTime }),
value || '0',
])),
);

// Turn raw data array into range (string) -> value (string) Maps
const activeUsers = array2map(data.activeUsers || []);
const failedExecutions = array2map(data.failedExecutions || []);
const totalExecutions = array2map(data.totalExecutions || []);

// Create a sorted array of unique time ranges
const timeRanges = Array.from(new Set([
...activeUsers.keys(),
...failedExecutions.keys(),
...totalExecutions.keys(),
])).sort().reverse();

// Turn the dataset into a table
const table = timeRanges.map(timeRange => {
const get = (map: Map<string, string>) => (map.get(timeRange) || '0');
return [
timeRange,
' ' + get(activeUsers),
get(activeUsers) === '1' ? 'user' : 'users',
' ' + get(totalExecutions),
get(totalExecutions) === '1' ? 'execution' : 'executions',
' ' + get(failedExecutions),
'failed',
];
});

const padders = [
String.prototype.padEnd, // for time range
String.prototype.padStart, // for number of user(s)
String.prototype.padEnd, // for 'user' / 'users'
String.prototype.padStart, // for number of executions
String.prototype.padEnd, // for 'execution' / 'executions'
String.prototype.padStart, // for number of failed executions
String.prototype.padEnd, // for 'failed'
];

// Determine padding for each column
const paddings = padders.map(
(_, columnIndex) => Math.max(...table.map(row => row[columnIndex].length)),
);

// Metrics API only supports UTC, and users might expect local time, let them know it's UTC.
console.error('UTC Date');

// Print results
for (const row of table) {
console.log(row.map((v, i) => padders[i].apply(v, [paddings[i]])).join(' '));
}
};
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import status from './commands/status';
import undeploy from './commands/undeploy';
import version from './commands/version';
import versions from './commands/versions';
import metrics from './commands/metrics';
import { handleError, PROJECT_NAME } from './utils';

// CLI
Expand Down Expand Up @@ -338,6 +339,16 @@ commander
.description('Update <settingKey> in .clasp.json')
.action(handleError(setting));

/**
* Show metrics
* @name metrics
* @example metrics
*/
commander
.command('metrics')
.description('Show metrics')
.action(handleError(metrics));

/**
* All other commands are given a help message.
* @example random
Expand Down
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ Forgot ${PROJECT_NAME} commands? Get help:\n ${PROJECT_NAME} --help`,
NO_NESTED_PROJECTS: '\nNested clasp projects are not supported.',
NO_VERSIONED_DEPLOYMENTS: 'No versioned deployments found in project.',
NO_WEBAPP: (deploymentId: string) => `Deployment "${deploymentId}" is not deployed as WebApp.`,
METRICS_UNEXPECTED_RANGE: 'Error: Unexpected time range returned by the Metrics API.',
OFFLINE: 'Error: Looks like you are offline.',
ONE_DEPLOYMENT_CREATE: 'Currently just one deployment can be created at a time.',
PAYLOAD_UNKNOWN: 'Unknown StackDriver payload.',
Expand Down Expand Up @@ -163,6 +164,7 @@ Cloned ${fileNum} ${pluralize('files', fileNum)}.`,
LOGIN: (isLocal: boolean) => `Logging in ${isLocal ? 'locally' : 'globally'}...`,
LOGS_SETUP: 'Finished setting up logs.\n',
NO_GCLOUD_PROJECT: `No projectId found. Running ${PROJECT_NAME} logs --setup.`,
METRICS: (scriptId: string) => `Getting metrics...`,
OPEN_CREDS: (projectId: string) => `Opening credentials page: ${URL.CREDS(projectId)}`,
OPEN_LINK: (link: string) => `Open this link: ${link}`,
OPEN_PROJECT: (scriptId: string) => `Opening script: ${URL.SCRIPT(scriptId)}`,
Expand Down
25 changes: 25 additions & 0 deletions tests/commands/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { spawnSync } from 'child_process';
import { expect } from 'chai';
import { describe, it } from 'mocha';
import {
CLASP,
} from '../constants';
import { cleanup, setup } from '../functions';

describe('Test clasp metrics function', () => {
before(setup);
it('should display metrics', () => {
const today = new Date();
const yesterday = new Date();
yesterday.setDate(today.getDate() - 1);

const result = spawnSync(
CLASP, ['metrics'], { encoding: 'utf8' },
);

expect(result.stderr).to.contain('UTC Date');
expect(result.stdout).to.contain(yesterday.toISOString().slice(0, 10));
expect(result.status).to.equal(0);
});
after(cleanup);
});

0 comments on commit 773cb54

Please sign in to comment.