Skip to content

Commit edeb405

Browse files
authored
fix: make downloads 30 day (#63)
1 parent 77a4dd4 commit edeb405

7 files changed

Lines changed: 110 additions & 19 deletions

File tree

env.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ declare module "*.txt" {
1111
declare namespace Cloudflare {
1212
interface Env {
1313
DPRINT_PLUGINS_GH_TOKEN?: string;
14+
// account id and a token with Account Analytics read access, used to query
15+
// download counts back out of the analytics engine dataset
16+
CLOUDFLARE_ACCOUNT_ID?: string;
17+
DPRINT_PLUGINS_ANALYTICS_TOKEN?: string;
1418
PLUGIN_CACHE: R2Bucket;
1519
DPRINT_PLUGIN_DOWNLOAD_ANALYTICS: AnalyticsEngineDataset;
1620
}

home.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function renderPlugins(data: PluginsData) {
7171
<div id="plugins-header">
7272
<div>Name</div>
7373
<div>Latest URL</div>
74-
<div>Downloads</div>
74+
<div>Downloads (30d)</div>
7575
<div></div>
7676
</div>
7777
{data.latest.map((plugin) => renderPlugin(plugin))}

plugins.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,10 @@ export async function getLatestInfo(username: string, repoName: string, origin:
149149
: `${origin}/${username}/${displayRepoName}-${releaseInfo.tagName}.${extension}`,
150150
version: releaseInfo.tagName.replace(/^v/, ""),
151151
checksum: releaseInfo.checksum,
152-
downloadCount: releaseInfo.downloadCount,
152+
// identifies this plugin's download analytics: the `username/repo` key that
153+
// downloads are recorded under and the tag of the latest release
154+
downloadKey: `${username}/${repoName}`,
155+
tag: releaseInfo.tagName,
153156
};
154157
}
155158

readInfoFile.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { env } from "cloudflare:workers";
22
import infoJson from "./info.json" with { type: "json" };
33
import { getLatestInfo } from "./plugins.js";
4-
import { getAllDownloadCount } from "./utils/github.js";
4+
import { getDownloadCounts, type PluginDownloadCounts } from "./utils/analytics.js";
55

66
// only typing what's used on the server
77
export interface PluginsData {
@@ -125,26 +125,35 @@ async function buildInfoFile(origin: string): Promise<Readonly<PluginsData>> {
125125
};
126126

127127
async function getLatest(latest: typeof infoJson.latest) {
128+
const downloadCounts = await getDownloadCounts();
128129
const results = [];
129130
for (const plugin of latest) {
130131
const [username, pluginName] = plugin.name.split("/");
131132
const info = pluginName
132133
? await getLatestInfo(username, pluginName, origin)
133134
: await getLatestInfo("dprint", plugin.name, origin);
134135
if (info != null) {
136+
const counts = downloadCounts.get(info.downloadKey);
135137
results.push({
136138
...plugin,
137139
version: info.version,
138140
url: info.url,
139141
downloadCount: {
140-
currentVersion: info.downloadCount,
141-
allVersions: pluginName
142-
? await getAllDownloadCount(username, pluginName)
143-
: await getAllDownloadCount("dprint", plugin.name),
142+
currentVersion: currentVersionDownloads(counts, info.tag),
143+
allVersions: counts?.allVersions ?? 0,
144144
},
145145
});
146146
}
147147
}
148148
return results;
149149
}
150150
}
151+
152+
// downloads of the latest release over the last 30 days, counting both the exact
153+
// version tag and the "latest" alias (which always resolves to the current release)
154+
function currentVersionDownloads(counts: PluginDownloadCounts | undefined, tag: string) {
155+
if (counts == null) {
156+
return 0;
157+
}
158+
return (counts.byTag.get(tag) ?? 0) + (counts.byTag.get("latest") ?? 0);
159+
}

utils/analytics.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { env } from "cloudflare:workers";
2+
import { fetchWithRetries } from "./fetchWithRetries.js";
3+
import { LazyExpirableValue } from "./LazyExpirableValue.js";
4+
5+
// download counts come from the Analytics Engine dataset that each plugin
6+
// download writes a data point to (see trackPluginDownload in handleRequest.ts).
7+
// the dataset only retains roughly the last 90 days, so these are downloads over
8+
// the trailing 30 days ("monthly") rather than all-time totals.
9+
10+
export interface PluginDownloadCounts {
11+
// downloads over the last 30 days across every version
12+
allVersions: number;
13+
// downloads over the last 30 days keyed by the tag that appeared in the request
14+
// url (a version like "0.1.0" or the "latest" alias)
15+
byTag: Map<string, number>;
16+
}
17+
18+
const DATASET = "dprint-plugin-downloads";
19+
const WINDOW_DAYS = 30;
20+
21+
const downloadCounts = new LazyExpirableValue<Map<string, PluginDownloadCounts>>({
22+
expiryMs: 5 * 60 * 1_000, // keep for 5 minutes
23+
createValue: queryDownloadCounts,
24+
});
25+
26+
/** Gets the download counts per plugin keyed by `username/repo`. */
27+
export async function getDownloadCounts(): Promise<Map<string, PluginDownloadCounts>> {
28+
try {
29+
return await downloadCounts.getValue();
30+
} catch (err) {
31+
// don't let analytics being unavailable break the info file build
32+
console.error("Failed to get download counts from analytics.", err);
33+
return new Map();
34+
}
35+
}
36+
37+
async function queryDownloadCounts(): Promise<Map<string, PluginDownloadCounts>> {
38+
const accountId = env.CLOUDFLARE_ACCOUNT_ID;
39+
const token = env.DPRINT_PLUGINS_ANALYTICS_TOKEN;
40+
const result = new Map<string, PluginDownloadCounts>();
41+
if (accountId == null || token == null) {
42+
console.warn("Analytics credentials not configured. Skipping download counts.");
43+
return result;
44+
}
45+
46+
// index1 is `username/repo`, blob1 is the tag, double1 is 1 per download
47+
const query = `SELECT index1 AS plugin, blob1 AS tag, sum(_sample_interval * double1) AS downloads`
48+
+ ` FROM "${DATASET}"`
49+
+ ` WHERE timestamp > NOW() - INTERVAL '${WINDOW_DAYS}' DAY`
50+
+ ` GROUP BY plugin, tag LIMIT 10000`;
51+
const response = await fetchWithRetries(
52+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/analytics_engine/sql`,
53+
{
54+
method: "POST",
55+
headers: {
56+
"authorization": `Bearer ${token}`,
57+
"content-type": "text/plain",
58+
},
59+
body: query,
60+
},
61+
);
62+
if (!response.ok) {
63+
const text = await response.text();
64+
throw new Error(`Analytics query failed: ${response.status}\n\n${text}`);
65+
}
66+
67+
const body = await response.json() as {
68+
data: { plugin: string; tag: string; downloads: number | string }[];
69+
};
70+
for (const row of body.data) {
71+
const downloads = Number(row.downloads) || 0;
72+
const counts = getOrCreateCounts(result, row.plugin);
73+
counts.allVersions += downloads;
74+
counts.byTag.set(row.tag, (counts.byTag.get(row.tag) ?? 0) + downloads);
75+
}
76+
return result;
77+
}
78+
79+
function getOrCreateCounts(map: Map<string, PluginDownloadCounts>, plugin: string) {
80+
let counts = map.get(plugin);
81+
if (counts == null) {
82+
counts = { allVersions: 0, byTag: new Map() };
83+
map.set(plugin, counts);
84+
}
85+
return counts;
86+
}

utils/github.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export interface ReleaseInfo {
3737
tagName: string;
3838
checksum: string | undefined;
3939
kind: "wasm" | "process";
40-
downloadCount: number;
4140
}
4241

4342
export async function getLatestReleaseInfo(username: string, repoName: string) {
@@ -54,7 +53,6 @@ function getReleaseInfo(data: GitHubRelease): ReleaseInfo {
5453
tagName: data.tag_name,
5554
checksum: getChecksum(),
5655
kind: getPluginKind(),
57-
downloadCount: getDownloadCount(data.assets),
5856
};
5957

6058
function getChecksum() {
@@ -93,21 +91,11 @@ function getReleaseInfo(data: GitHubRelease): ReleaseInfo {
9391
}
9492
}
9593

96-
function getDownloadCount(assets: ReleaseAsset[]) {
97-
return assets.find(({ name }) => name === "plugin.wasm" || name === "plugin.json")?.download_count ?? 0;
98-
}
99-
10094
interface ReleaseAsset {
10195
name: string;
102-
download_count: number;
10396
digest: string | null;
10497
}
10598

106-
export async function getAllDownloadCount(username: string, repoName: string) {
107-
const releases = await getReleasesData(username, repoName);
108-
return releases?.reduce((total, current) => total + getDownloadCount(current.assets), 0) ?? 0;
109-
}
110-
11199
interface GitHubRelease {
112100
tag_name: string;
113101
body: string;

utils/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from "./analytics.js";
12
export * from "./asyncLazy.js";
23
export * from "./github.js";
34
export * from "./version.js";

0 commit comments

Comments
 (0)