Skip to content

Commit ba6db3e

Browse files
committed
Send anonymouse usage reporting to Grafana
1 parent b77618e commit ba6db3e

File tree

5 files changed

+140
-3
lines changed

5 files changed

+140
-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

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)