Skip to content

Commit 222d6cc

Browse files
committed
Send anonymouse usage reporting to Grafana
1 parent 9570743 commit 222d6cc

File tree

6 files changed

+245
-3
lines changed

6 files changed

+245
-3
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ The following inputs can be used as `step.with` key:
3333
| `cloud-comment-on-pr` | boolean | `false` | `true` | If `true`, the workflow comments a link to the cloud test run on the pull request (if present) |
3434
| `only-verify-scripts` | boolean | `false` | `false` | If `true`, only check if the test scripts are valid and skip the test execution' |
3535
| `debug` | boolean | `false` | `false` | If true, the output from k6 will be shown in the action logs, else only the summary will be shown. |
36-
36+
| `disable-analytics` | boolean | `false` | `false` | If true, the anonymous usage analytics reporting will be disabled |
3737
## Usage
3838

3939
Following are some examples of using the workflow.

action.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ inputs:
4545
description: 'If true, the output from k6 will be shown in the action logs, else only the summary will be shown'
4646
default: "false"
4747
required: false
48+
disable-analytics:
49+
description: 'If true, the anonymous usage analytics reporting will be disabled'
50+
default: "false"
51+
required: false
4852

4953
runs:
5054
using: node20

dist/index.js

+105
Original file line numberDiff line numberDiff line change
@@ -35097,6 +35097,64 @@ function wrappy (fn, cb) {
3509735097
}
3509835098

3509935099

35100+
/***/ }),
35101+
35102+
/***/ 1267:
35103+
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
35104+
35105+
"use strict";
35106+
35107+
var __importDefault = (this && this.__importDefault) || function (mod) {
35108+
return (mod && mod.__esModule) ? mod : { "default": mod };
35109+
};
35110+
Object.defineProperty(exports, "__esModule", ({ value: true }));
35111+
exports.sendAnalytics = sendAnalytics;
35112+
const crypto_1 = __importDefault(__nccwpck_require__(6982));
35113+
const os_1 = __importDefault(__nccwpck_require__(857));
35114+
const apiUtils_1 = __nccwpck_require__(890);
35115+
const k6helper_1 = __nccwpck_require__(5354);
35116+
const ANALYTICS_SOURCE = 'github-action';
35117+
/**
35118+
* Gets the usage stats id which is an identifier for the invocation of the action
35119+
* Here we use a hash of GITHUB_ACTION and GITHUB_REPOSITORY to identify the unique users and
35120+
* club multiple invocations from the same user/repo
35121+
*
35122+
* @returns The usage stats id
35123+
*/
35124+
function getUsageStatsId() {
35125+
const githubAction = process.env.GITHUB_ACTION || '';
35126+
const githubWorkflow = process.env.GITHUB_WORKFLOW || '';
35127+
return crypto_1.default
35128+
.createHash('sha256')
35129+
.update(`${githubAction}-${githubWorkflow}`)
35130+
.digest('hex');
35131+
}
35132+
async function sendAnalytics(userSpecifiedAnalyticsData) {
35133+
const analyticsData = {
35134+
...userSpecifiedAnalyticsData,
35135+
source: ANALYTICS_SOURCE,
35136+
usageStatsId: getUsageStatsId(),
35137+
osPlatform: os_1.default.platform(),
35138+
osArch: os_1.default.arch(),
35139+
osType: os_1.default.type(),
35140+
k6Version: (0, k6helper_1.getInstalledK6Version)(),
35141+
};
35142+
const url = process.env.GRAFANA_ANALYTICS_URL || 'https://stats.grafana.org';
35143+
try {
35144+
await (0, apiUtils_1.apiRequest)(url, {
35145+
method: 'POST',
35146+
body: JSON.stringify(analyticsData),
35147+
}, {
35148+
...apiUtils_1.DEFAULT_RETRY_OPTIONS,
35149+
maxRetries: 1,
35150+
});
35151+
}
35152+
catch (error) {
35153+
console.error('Error sending analytics:', error);
35154+
}
35155+
}
35156+
35157+
3510035158
/***/ }),
3510135159

3510235160
/***/ 890:
@@ -35494,6 +35552,7 @@ var __importStar = (this && this.__importStar) || (function () {
3549435552
Object.defineProperty(exports, "__esModule", ({ value: true }));
3549535553
exports.run = run;
3549635554
const core = __importStar(__nccwpck_require__(7484));
35555+
const analytics_1 = __nccwpck_require__(1267);
3549735556
const githubHelper_1 = __nccwpck_require__(2384);
3549835557
const k6helper_1 = __nccwpck_require__(5354);
3549935558
const utils_1 = __nccwpck_require__(1798);
@@ -35514,6 +35573,7 @@ async function run() {
3551435573
const onlyVerifyScripts = core.getBooleanInput('only-verify-scripts');
3551535574
const shouldCommentCloudTestRunUrlOnPR = core.getBooleanInput('cloud-comment-on-pr');
3551635575
const debug = core.getBooleanInput('debug');
35576+
const disableAnalytics = core.getBooleanInput('disable-analytics');
3551735577
const allPromises = [];
3551835578
core.debug(`Flag to show k6 progress output set to: ${debug}`);
3551935579
core.debug(`🔍 Found following ${testPaths.length} test run files:`);
@@ -35536,6 +35596,20 @@ async function run() {
3553635596
return;
3553735597
}
3553835598
const isCloud = (0, k6helper_1.isCloudIntegrationEnabled)();
35599+
if (!disableAnalytics) {
35600+
const userSpecifiedAnalyticsData = {
35601+
totalTestScriptsExecuted: verifiedTestPaths.length,
35602+
isCloudRun: isCloud,
35603+
isUsingFlags: flags.length > 0,
35604+
isUsingInspectFlags: inspectFlags.length > 0,
35605+
failFast,
35606+
commentOnPr: shouldCommentCloudTestRunUrlOnPR,
35607+
parallelFlag: parallel,
35608+
cloudRunLocally,
35609+
onlyVerifyScripts,
35610+
};
35611+
(0, analytics_1.sendAnalytics)(userSpecifiedAnalyticsData);
35612+
}
3553935613
const commands = testPaths.map((testPath) => (0, k6helper_1.generateK6RunCommand)(testPath, flags, isCloud, cloudRunLocally)), TOTAL_TEST_RUNS = commands.length, TEST_RESULT_URLS_MAP = new Proxy({}, {
3554035614
set: (target, key, value) => {
3554135615
target[key] = value;
@@ -35867,6 +35941,8 @@ exports.executeRunK6Command = executeRunK6Command;
3586735941
exports.extractTestRunId = extractTestRunId;
3586835942
exports.fetchTestRunSummary = fetchTestRunSummary;
3586935943
exports.fetchChecks = fetchChecks;
35944+
exports.extractK6SemVer = extractK6SemVer;
35945+
exports.getInstalledK6Version = getInstalledK6Version;
3587035946
// Common helper functions used in the action
3587135947
const core = __importStar(__nccwpck_require__(7484));
3587235948
const child_process_1 = __nccwpck_require__(5317);
@@ -36049,6 +36125,35 @@ async function fetchChecks(testRunId) {
3604936125
// Return the checks array from the response
3605036126
return response.value;
3605136127
}
36128+
/**
36129+
* Extracts the semantic version (e.g., "0.56.0") from the full k6 version string which looks like
36130+
* `k6 v0.56.0 (go1.23.4, darwin/arm64)`.
36131+
*
36132+
* @param {string} versionString - The full version string from k6 version command
36133+
* @returns {string} The semantic version or empty string if not found
36134+
*/
36135+
function extractK6SemVer(versionString) {
36136+
// Match pattern like "v0.56.0" and extract just the digits and dots
36137+
const match = versionString.match(/v(\d+\.\d+\.\d+)/);
36138+
return match ? match[1] : '';
36139+
}
36140+
/**
36141+
* Gets the installed k6 version using the `k6 version` command.
36142+
*
36143+
* @returns The installed k6 version as a semantic version string
36144+
*/
36145+
function getInstalledK6Version() {
36146+
try {
36147+
// Use execSync for synchronous output capture
36148+
const output = (0, child_process_1.execSync)('k6 version').toString().trim();
36149+
// Return only the semantic version if requested
36150+
return extractK6SemVer(output);
36151+
}
36152+
catch (error) {
36153+
console.error('Error executing k6 version:', error);
36154+
return '';
36155+
}
36156+
}
3605236157

3605336158

3605436159
/***/ }),

src/analytics.ts

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import crypto from 'crypto'
2+
import os from 'os'
3+
import { apiRequest, DEFAULT_RETRY_OPTIONS } from './apiUtils'
4+
import { getInstalledK6Version } from './k6helper'
5+
6+
const ANALYTICS_SOURCE = 'github-action'
7+
8+
export interface UserSpecifiedAnalyticsData {
9+
totalTestScriptsExecuted: number
10+
isCloudRun: boolean
11+
isUsingFlags: boolean
12+
isUsingInspectFlags: boolean
13+
failFast: boolean
14+
commentOnPr: boolean
15+
parallelFlag: boolean
16+
cloudRunLocally: boolean
17+
onlyVerifyScripts: boolean
18+
}
19+
20+
interface AnalyticsData {
21+
source: string
22+
usageStatsId: string
23+
osPlatform: string
24+
osArch: string
25+
osType: string
26+
k6Version: string
27+
28+
totalTestScriptsExecuted: number
29+
isCloudRun: boolean
30+
isUsingFlags: boolean
31+
isUsingInspectFlags: boolean
32+
failFast: boolean
33+
commentOnPr: boolean
34+
parallelFlag: boolean
35+
cloudRunLocally: boolean
36+
onlyVerifyScripts: boolean
37+
}
38+
39+
/**
40+
* Gets the usage stats id which is an identifier for the invocation of the action
41+
* Here we use a hash of GITHUB_ACTION and GITHUB_REPOSITORY to identify the unique users and
42+
* club multiple invocations from the same user/repo
43+
*
44+
* @returns The usage stats id
45+
*/
46+
function getUsageStatsId(): string {
47+
const githubAction = process.env.GITHUB_ACTION || ''
48+
const githubWorkflow = process.env.GITHUB_WORKFLOW || ''
49+
return crypto
50+
.createHash('sha256')
51+
.update(`${githubAction}-${githubWorkflow}`)
52+
.digest('hex')
53+
}
54+
55+
export async function sendAnalytics(
56+
userSpecifiedAnalyticsData: UserSpecifiedAnalyticsData
57+
) {
58+
const analyticsData: AnalyticsData = {
59+
...userSpecifiedAnalyticsData,
60+
source: ANALYTICS_SOURCE,
61+
usageStatsId: getUsageStatsId(),
62+
osPlatform: os.platform(),
63+
osArch: os.arch(),
64+
osType: os.type(),
65+
k6Version: getInstalledK6Version(),
66+
}
67+
68+
const url = process.env.GRAFANA_ANALYTICS_URL || 'https://stats.grafana.org'
69+
70+
try {
71+
await apiRequest(
72+
url,
73+
{
74+
method: 'POST',
75+
body: JSON.stringify(analyticsData),
76+
},
77+
{
78+
...DEFAULT_RETRY_OPTIONS,
79+
maxRetries: 1,
80+
}
81+
)
82+
} catch (error) {
83+
console.error('Error sending analytics:', error)
84+
}
85+
}

src/index.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as core from '@actions/core'
22
import { ChildProcess } from 'child_process'
33

4+
import { sendAnalytics, UserSpecifiedAnalyticsData } from './analytics'
45
import { generatePRComment } from './githubHelper'
56
import {
67
cleanScriptPath,
@@ -35,7 +36,7 @@ export async function run(): Promise<void> {
3536
'cloud-comment-on-pr'
3637
)
3738
const debug = core.getBooleanInput('debug')
38-
39+
const disableAnalytics = core.getBooleanInput('disable-analytics')
3940
const allPromises: Promise<void>[] = []
4041

4142
core.debug(`Flag to show k6 progress output set to: ${debug}`)
@@ -72,6 +73,22 @@ export async function run(): Promise<void> {
7273

7374
const isCloud = isCloudIntegrationEnabled()
7475

76+
if (!disableAnalytics) {
77+
const userSpecifiedAnalyticsData: UserSpecifiedAnalyticsData = {
78+
totalTestScriptsExecuted: verifiedTestPaths.length,
79+
isCloudRun: isCloud,
80+
isUsingFlags: flags.length > 0,
81+
isUsingInspectFlags: inspectFlags.length > 0,
82+
failFast,
83+
commentOnPr: shouldCommentCloudTestRunUrlOnPR,
84+
parallelFlag: parallel,
85+
cloudRunLocally,
86+
onlyVerifyScripts,
87+
}
88+
89+
sendAnalytics(userSpecifiedAnalyticsData)
90+
}
91+
7592
const commands = testPaths.map((testPath) =>
7693
generateK6RunCommand(testPath, flags, isCloud, cloudRunLocally)
7794
),

src/k6helper.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Common helper functions used in the action
22
import * as core from '@actions/core'
3-
import { ChildProcess, spawn } from 'child_process'
3+
import { ChildProcess, execSync, spawn } from 'child_process'
44
import path from 'path'
55
import { apiRequest } from './apiUtils'
66
import { parseK6Output } from './k6OutputParser'
@@ -243,3 +243,34 @@ export async function fetchChecks(testRunId: string): Promise<Check[]> {
243243
// Return the checks array from the response
244244
return response.value
245245
}
246+
247+
/**
248+
* Extracts the semantic version (e.g., "0.56.0") from the full k6 version string which looks like
249+
* `k6 v0.56.0 (go1.23.4, darwin/arm64)`.
250+
*
251+
* @param {string} versionString - The full version string from k6 version command
252+
* @returns {string} The semantic version or empty string if not found
253+
*/
254+
export function extractK6SemVer(versionString: string): string {
255+
// Match pattern like "v0.56.0" and extract just the digits and dots
256+
const match = versionString.match(/v(\d+\.\d+\.\d+)/)
257+
return match ? match[1] : ''
258+
}
259+
260+
/**
261+
* Gets the installed k6 version using the `k6 version` command.
262+
*
263+
* @returns The installed k6 version as a semantic version string
264+
*/
265+
export function getInstalledK6Version(): string {
266+
try {
267+
// Use execSync for synchronous output capture
268+
const output = execSync('k6 version').toString().trim()
269+
270+
// Return only the semantic version if requested
271+
return extractK6SemVer(output)
272+
} catch (error) {
273+
console.error('Error executing k6 version:', error)
274+
return ''
275+
}
276+
}

0 commit comments

Comments
 (0)