Skip to content

Commit 2f2a141

Browse files
authored
Docstat: CLI tool to fetch metrics from the Kusto API (#55917)
1 parent b717751 commit 2f2a141

File tree

14 files changed

+1427
-11
lines changed

14 files changed

+1427
-11
lines changed

package-lock.json

Lines changed: 511 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@
256256
"ajv": "^8.17.1",
257257
"ajv-errors": "^3.0.0",
258258
"ajv-formats": "^3.0.1",
259+
"azure-kusto-data": "^7.0.0",
259260
"bottleneck": "2.19.5",
260261
"boxen": "8.0.1",
261262
"cheerio": "^1.0.0-rc.12",

src/metrics/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Kusto tooling
2+
3+
CLI tools to fetch data from the Kusto API.
4+
5+
## Installation and authentication
6+
7+
1. Install the Azure CLI with `brew install azure-cli`.
8+
* If you have the option to **not** update all your brew packages, choose that, or it will take a really long time.
9+
1. Run `az login`.
10+
* You'll have to run `az login` whenever your session expires. The sessions are fairly long lasting.
11+
1. Enter your `<username>@githubazure.com` credentials.
12+
* These will get cached for future logins.
13+
1. At the prompt in Terminal asking which subscription you want to use, just press Enter to choose the default.
14+
1. Open or create an `.env` file in the root directory of your checkout (this file is already in `.gitignore`).
15+
1. Add the `KUSTO_CLUSTER` and `KUSTO_DATABASE` values to the `.env`.
16+
```
17+
KUSTO_CLUSTER='<value>'
18+
KUSTO_DATABASE='<value>'
19+
```

src/metrics/lib/dates.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const dateOpts = {
2+
year: 'numeric',
3+
month: 'long',
4+
day: 'numeric',
5+
}
6+
7+
// Default to 30 days ago if a range option is not provided
8+
export function getDates(range = '30') {
9+
// Get current datetime in ISO format
10+
const today = new Date()
11+
const todayISO = today.toISOString()
12+
13+
// Get datetime from N days ago in ISO format
14+
const daysAgo = getDaysAgo(Number(range))
15+
const daysAgoISO = daysAgo.toISOString()
16+
17+
return {
18+
endDate: todayISO,
19+
startDate: daysAgoISO,
20+
friendlyRange: `${daysAgo.toLocaleDateString('en-US', dateOpts)} - ${today.toLocaleDateString('en-US', dateOpts)}`,
21+
}
22+
}
23+
24+
function getDaysAgo(range) {
25+
const daysAgo = new Date()
26+
daysAgo.setDate(daysAgo.getDate() - range)
27+
return daysAgo
28+
}

src/metrics/lib/kusto-client.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Client as KustoClient, KustoConnectionStringBuilder } from 'azure-kusto-data'
2+
3+
import dotenv from 'dotenv'
4+
dotenv.config()
5+
if (!(process.env.KUSTO_CLUSTER || process.env.KUSTO_DATABASE)) {
6+
console.error(`Add KUSTO_CLUSTER and KUSTO_DATABASE to your .env file`)
7+
process.exit(0)
8+
}
9+
const KUSTO_CLUSTER = process.env.KUSTO_CLUSTER
10+
const KUSTO_DATABASE = process.env.KUSTO_DATABASE
11+
12+
export function getKustoClient() {
13+
let client
14+
try {
15+
const kcsb = KustoConnectionStringBuilder.withAzLoginIdentity(KUSTO_CLUSTER)
16+
client = new KustoClient(kcsb)
17+
} catch (error) {
18+
console.error('Error connecting to Kusto')
19+
console.error(error)
20+
}
21+
return client
22+
}
23+
24+
export async function runQuery(pathToFetch, query, client, queryType, verbose = false) {
25+
// Display query if verbose mode is on
26+
if (verbose) {
27+
console.log(`\n--- EXECUTING QUERY FOR "${queryType.toUpperCase()}" ---`)
28+
console.log(query)
29+
console.log('----------------------\n')
30+
}
31+
32+
const results = await client.execute(KUSTO_DATABASE, query)
33+
34+
if (results.primaryResults.length === 0) {
35+
console.log(`No data found for URL: ${pathToFetch}`)
36+
return null
37+
}
38+
39+
return results
40+
}

src/metrics/queries/bounces.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { runQuery } from '#src/metrics/lib/kusto-client.js'
2+
import { SHARED_DECLARATIONS, SHARED_FILTERS } from '#src/metrics/queries/constants.js'
3+
4+
const QUERY_TYPE = 'bounces'
5+
6+
export async function getBounces(
7+
pathToFetch,
8+
client,
9+
dates,
10+
version = null,
11+
verbose = false,
12+
queryType = QUERY_TYPE,
13+
) {
14+
const query = getBouncesQuery(pathToFetch, dates, version)
15+
const results = await runQuery(pathToFetch, query, client, queryType, verbose)
16+
const data = JSON.parse(results.primaryResults[0].toString()).data[0]
17+
// Extract Bounces
18+
const bounces = data.Bounces
19+
return bounces
20+
}
21+
22+
export function getBouncesQuery(pathToFetch, dates, version) {
23+
return `
24+
${SHARED_DECLARATIONS(pathToFetch, dates, version)}
25+
let _exits = () {
26+
docs_v0_exit_event
27+
${SHARED_FILTERS}
28+
};
29+
_exits
30+
| summarize Bounces=round(
31+
countif(exit_scroll_length < 0.1 and exit_visit_duration < 5) /
32+
toreal(count()),
33+
2
34+
)
35+
| project Bounces=strcat(toint(
36+
Bounces * 100
37+
), '%')
38+
`
39+
}

src/metrics/queries/constants.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// SHARED QUERY CONSTANTS
2+
export const SHARED_DECLARATIONS = (path, dates, version) =>
3+
`
4+
let _article = dynamic(['${Array.isArray(path) ? path.join("', '") : path}']);
5+
let _articleType = dynamic(null);
6+
let _endTime = datetime(${dates.endDate || null});
7+
let _language = dynamic(null);
8+
let _pageType = dynamic(null);
9+
let _product = dynamic(null);
10+
let _startTime = datetime(${dates.startDate || null});
11+
let _version = dynamic(${version ? `['${version}']` : null});
12+
`.trim()
13+
14+
export const SHARED_FILTERS = `
15+
| where timestamp between (_startTime .. _endTime)
16+
| where context.hostname == 'docs.github.com'
17+
| where abs(totimespan(context.created - timestamp)) < 1h
18+
| where isempty(_language) or tostring(context.path_language) in (_language)
19+
| where isempty(_version) or tostring(context.path_version) in (_version)
20+
| where isempty(_article) or context.path_article has_any (_article)
21+
| where isempty(_product) or tostring(context.path_product) in (_product)
22+
| where isempty(_pageType) or tostring(context.page_document_type) in (_pageType)
23+
| where isempty(_articleType) or tostring(context.page_type) in (_articleType)
24+
`.trim()
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { runQuery } from '#src/metrics/lib/kusto-client.js'
2+
import { SHARED_DECLARATIONS, SHARED_FILTERS } from '#src/metrics/queries/constants.js'
3+
4+
const QUERY_TYPE = 'exits'
5+
6+
export async function getExitsToSupport(
7+
pathToFetch,
8+
client,
9+
dates,
10+
version = null,
11+
verbose = false,
12+
queryType = QUERY_TYPE,
13+
) {
14+
const query = getExitsQueryStatement(pathToFetch, dates, version)
15+
const results = await runQuery(pathToFetch, query, client, queryType, verbose)
16+
const data = JSON.parse(results.primaryResults[0].toString()).data[0]
17+
// Extract Column1
18+
const exitsToSupport = data.Column1
19+
return exitsToSupport
20+
}
21+
22+
export function getExitsQueryStatement(pathToFetch, dates, version) {
23+
return `
24+
${SHARED_DECLARATIONS(pathToFetch, dates, version)}
25+
let _links = () {
26+
docs_v0_link_event
27+
${SHARED_FILTERS}
28+
};
29+
_links
30+
| where isempty(link_samesite) or link_samesite == false
31+
| where link_samepage != true // allow false or null
32+
| extend link_url_parsed=parse_url(link_url)
33+
| extend IsSupport=tostring(link_url_parsed.Host) == "support.github.com"
34+
and tostring(link_url_parsed.path) != "/enterprise/server-upgrade"
35+
| summarize Ratio=round(countif(IsSupport) / toreal(count()), 2)
36+
| project strcat(toint(Ratio * 100), '%')
37+
`
38+
}

src/metrics/queries/survey-score.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { runQuery } from '#src/metrics/lib/kusto-client.js'
2+
import { SHARED_DECLARATIONS, SHARED_FILTERS } from '#src/metrics/queries/constants.js'
3+
4+
const QUERY_TYPE = 'score'
5+
6+
export async function getScore(
7+
pathToFetch,
8+
client,
9+
dates,
10+
version = null,
11+
verbose = false,
12+
queryType = QUERY_TYPE,
13+
) {
14+
const query = getScoreQuery(pathToFetch, dates, version)
15+
const results = await runQuery(pathToFetch, query, client, queryType, verbose)
16+
const data = JSON.parse(results.primaryResults[0].toString()).data[0]
17+
// Extract Score
18+
const score = data.Score
19+
return score
20+
}
21+
22+
export function getScoreQuery(pathToFetch, dates, version) {
23+
return `
24+
${SHARED_DECLARATIONS(pathToFetch, dates, version)}
25+
let _surveys = () {
26+
docs_v0_survey_event
27+
${SHARED_FILTERS}
28+
// Filter out Copilot response thumbs up/down events
29+
| where context.event_group_key != "ask-ai"
30+
// UNIQUE DO NOT DELETE
31+
| summarize timestamp=arg_max(timestamp, *)
32+
by User=toguid(context.user), Path=tostring(context.path_article)
33+
};
34+
_surveys
35+
| summarize Score=round(
36+
(countif(survey_vote) + 0.75 * 30)
37+
/ (count() + 30),
38+
2
39+
)
40+
| project Score=strcat(toint(Score * 100), '%')
41+
`
42+
}

src/metrics/queries/users.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { runQuery } from '#src/metrics/lib/kusto-client.js'
2+
import { SHARED_DECLARATIONS, SHARED_FILTERS } from '#src/metrics/queries/constants.js'
3+
4+
const QUERY_TYPE = 'users'
5+
6+
export async function getUsers(
7+
pathToFetch,
8+
client,
9+
dates,
10+
version = null,
11+
verbose = false,
12+
queryType = QUERY_TYPE,
13+
) {
14+
const query = getUsersQuery(pathToFetch, dates, version)
15+
const results = await runQuery(pathToFetch, query, client, queryType, verbose)
16+
const data = JSON.parse(results.primaryResults[0].toString()).data[0]
17+
// Extract Users
18+
const users = data.Users
19+
return users.toLocaleString()
20+
}
21+
22+
export function getUsersQuery(pathToFetch, dates, version) {
23+
return `
24+
${SHARED_DECLARATIONS(pathToFetch, dates, version)}
25+
let _pages = () {
26+
docs_v0_page_event
27+
${SHARED_FILTERS}
28+
};
29+
_pages
30+
| summarize Users=dcount(tostring(context.user))
31+
`
32+
}

0 commit comments

Comments
 (0)