Skip to content

Commit e98340a

Browse files
committed
feat(tracker): enhance deployment workflow and remove connection tracking
- Add pre-deployment hash checking to show which files will be updated - Add Discord webhook notifications for deployments with customizable messages - Add Pull Zone cache purging after successful deployments - Add --purge flag to purge cache without deploying - Add --skip-notification flag to skip Discord notifications - Add --message flag for deployment notes in Discord notifications - Remove connection info tracking (connection_type, rtt, downlink) from events - Fix data attribute parsing to handle boolean attributes without values (e.g., data-use-pixel) - Improve deployment UX with better status messages and file change detection
1 parent 9ce91fe commit e98340a

File tree

1 file changed

+196
-30
lines changed

1 file changed

+196
-30
lines changed

packages/tracker/deploy.ts

Lines changed: 196 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,28 @@ program
1212
.option("-d, --dry-run", "Simulate the deployment without uploading files")
1313
.option("-y, --yes", "Skip confirmation prompt")
1414
.option("-f, --force", "Force upload even if hash matches")
15+
.option("-m, --message <text>", "Add a note to the deployment notification")
16+
.option("--skip-notification", "Skip sending Discord notification")
17+
.option("-p, --purge", "Only purge cache, skip deployment")
1518
.option("-v, --verbose", "Enable verbose logging")
1619
.parse(process.argv);
1720

1821
const options = program.opts<{
1922
dryRun: boolean;
2023
yes: boolean;
2124
force: boolean;
25+
message?: string;
26+
skipNotification?: boolean;
27+
purge?: boolean;
2228
verbose: boolean;
2329
}>();
2430

2531
const STORAGE_ZONE_NAME = process.env.BUNNY_STORAGE_ZONE_NAME;
2632
const ACCESS_KEY = process.env.BUNNY_STORAGE_ACCESS_KEY;
33+
const API_KEY = process.env.BUNNY_API_KEY;
34+
const PULL_ZONE_ID = process.env.BUNNY_PULL_ZONE_ID;
2735
const REGION = process.env.BUNNY_STORAGE_REGION || "";
36+
const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL;
2837
const PUBLIC_CDN_URL = "https://databuddy.b-cdn.net";
2938

3039
if (!STORAGE_ZONE_NAME) {
@@ -61,37 +70,52 @@ async function fetchRemoteHash(filename: string): Promise<string | null> {
6170
}
6271
}
6372

64-
async function uploadFile(filename: string) {
73+
async function checkFileStatus(filename: string): Promise<{
74+
filename: string;
75+
status: "changed" | "same" | "new" | "error";
76+
size: number;
77+
content?: string;
78+
}> {
6579
const filePath = join(DIST_DIR, filename);
6680
const fileContent = file(filePath);
6781

6882
if (!(await fileContent.exists())) {
69-
console.warn(chalk.yellow(`⚠️ File not found: ${filename}`));
70-
return;
83+
return { filename, status: "error", size: 0 };
7184
}
7285

7386
const content = await fileContent.text();
7487
const localHash = getHash(content);
7588
const remoteHash = await fetchRemoteHash(filename);
89+
const size = (await fileContent.size) / 1024; // KB
7690

77-
if (remoteHash === localHash && !options.force) {
78-
if (options.verbose) {
79-
console.log(chalk.gray(`⏭️ Skipping ${filename} (hash match)`));
80-
} else {
81-
console.log(chalk.gray(`⏭️ ${filename}`));
82-
}
83-
return;
91+
if (!remoteHash) {
92+
return { filename, status: "new", size, content };
93+
}
94+
95+
if (remoteHash !== localHash || options.force) {
96+
return { filename, status: "changed", size, content };
8497
}
8598

99+
return { filename, status: "same", size };
100+
}
101+
102+
async function uploadFile(
103+
filename: string,
104+
content: string,
105+
size: number
106+
): Promise<{
107+
filename: string;
108+
status: "uploaded" | "dry-run" | "error";
109+
size: number;
110+
}> {
86111
const url = `${BASE_URL}/${STORAGE_ZONE_NAME}/${filename}`;
87-
const size = (await fileContent.size) / 1024; // KB
88112

89113
if (options.dryRun) {
90114
console.log(
91115
chalk.cyan(`[DRY RUN] Would upload ${chalk.bold(filename)}`) +
92116
chalk.dim(` (${size.toFixed(2)} KB) to ${url}`)
93117
);
94-
return;
118+
return { filename, status: "dry-run", size };
95119
}
96120

97121
if (options.verbose) {
@@ -106,7 +130,7 @@ async function uploadFile(filename: string) {
106130
AccessKey: ACCESS_KEY as string,
107131
"Content-Type": "application/javascript",
108132
},
109-
body: content, // Use text content to match hash calculation
133+
body: content,
110134
});
111135

112136
if (!response.ok) {
@@ -118,9 +142,97 @@ async function uploadFile(filename: string) {
118142
console.log(
119143
chalk.green(`✅ Uploaded ${filename}`) + chalk.dim(` in ${duration}ms`)
120144
);
145+
return { filename, status: "uploaded", size };
121146
} catch (error) {
122147
console.error(chalk.red(`❌ Failed to upload ${filename}:`), error);
123-
process.exit(1);
148+
return { filename, status: "error", size };
149+
}
150+
}
151+
152+
async function sendDiscordNotification(
153+
uploadedFiles: { filename: string; size: number }[],
154+
message?: string
155+
) {
156+
if (!DISCORD_WEBHOOK_URL) {
157+
return;
158+
}
159+
160+
try {
161+
const totalSize = uploadedFiles.reduce((acc, f) => acc + f.size, 0);
162+
const fileList = uploadedFiles
163+
.map((f) => `- **${f.filename}** (${f.size.toFixed(2)} KB)`)
164+
.join("\n");
165+
166+
const embed = {
167+
title: "Tracker Scripts Deployed",
168+
description: message
169+
? `> ${message}`
170+
: "A new version of the tracker scripts has been deployed to the CDN.",
171+
color: 5_763_719, // Green
172+
fields: [
173+
{
174+
name: "Updated Files",
175+
value: fileList,
176+
inline: false,
177+
},
178+
{
179+
name: "Deployment Stats",
180+
value: `**Total Size:** ${totalSize.toFixed(2)} KB\n**Files:** ${uploadedFiles.length}\n**Environment:** Production`,
181+
inline: false,
182+
},
183+
],
184+
timestamp: new Date().toISOString(),
185+
footer: {
186+
text: "Databuddy Tracker Deployment",
187+
},
188+
};
189+
190+
await fetch(DISCORD_WEBHOOK_URL, {
191+
method: "POST",
192+
headers: { "Content-Type": "application/json" },
193+
body: JSON.stringify({ embeds: [embed] }),
194+
});
195+
console.log(chalk.blue("\n📨 Discord notification sent"));
196+
} catch (error) {
197+
console.error(
198+
chalk.yellow("⚠️ Failed to send Discord notification:"),
199+
error
200+
);
201+
}
202+
}
203+
204+
async function purgePullZoneCache() {
205+
if (!(API_KEY && PULL_ZONE_ID)) {
206+
console.warn(
207+
chalk.yellow(
208+
"⚠️ Missing BUNNY_API_KEY or BUNNY_PULL_ZONE_ID. Skipping cache purge."
209+
)
210+
);
211+
return;
212+
}
213+
214+
try {
215+
const url = `https://api.bunny.net/pullzone/${PULL_ZONE_ID}/purgeCache`;
216+
const response = await fetch(url, {
217+
method: "POST",
218+
headers: {
219+
AccessKey: API_KEY,
220+
"Content-Type": "application/json",
221+
},
222+
});
223+
224+
if (response.status === 204 || response.ok) {
225+
console.log(chalk.green("🧹 Successfully purged Pull Zone cache"));
226+
} else {
227+
const text = await response.text();
228+
console.error(
229+
chalk.red(
230+
`❌ Failed to purge Pull Zone cache: ${response.status} - ${text}`
231+
)
232+
);
233+
}
234+
} catch (error) {
235+
console.error(chalk.red("❌ Failed to purge Pull Zone cache:"), error);
124236
}
125237
}
126238

@@ -147,25 +259,43 @@ async function deploy() {
147259
console.log(chalk.dim(`Files: ${jsFiles.join(", ")}`));
148260
}
149261

150-
// Only prompt if not skipping checks and there are actual changes to deploy
151-
// But we need to check hashes first to know if there are changes.
152-
// For simplicity, we'll iterate files, check hash, and upload/skip.
153-
// The prompt is "Are you sure you want to deploy these files?" implies ALL files.
154-
// Let's keep the prompt before starting the process.
262+
// Check file statuses first
263+
console.log(chalk.dim("Checking for changes..."));
264+
const fileStatuses = await Promise.all(jsFiles.map(checkFileStatus));
265+
266+
const changedFiles = fileStatuses.filter(
267+
(f) => f.status === "changed" || f.status === "new"
268+
);
269+
270+
if (changedFiles.length === 0) {
271+
console.log(
272+
chalk.green("✨ No changes detected. Everything is up to date.")
273+
);
274+
return;
275+
}
276+
277+
console.log(
278+
chalk.bold(
279+
`\n📦 Found ${changedFiles.length} files to update in ${chalk.cyan(STORAGE_ZONE_NAME)}:`
280+
)
281+
);
282+
283+
for (const file of changedFiles) {
284+
const icon = file.status === "new" ? "🆕" : "🔄";
285+
console.log(
286+
` ${icon} ${chalk.white(file.filename)} ${chalk.dim(
287+
`(${file.size.toFixed(2)} KB)`
288+
)}`
289+
);
290+
}
155291

156292
const skipConfirmation = options.yes || options.dryRun;
157293

158294
if (!skipConfirmation) {
159-
// Ideally we would pre-calculate what NEEDS uploading, but that requires fetching all remote hashes first.
160-
// Let's do a quick check or just prompt generally.
161-
// Given the request is just "skip uploading if hash matches", we can do it per-file.
162-
// But user might want to know WHAT will be uploaded before confirming.
163-
// For now, let's keep the simple flow: Prompt -> Iterate & Check/Upload.
164-
165295
const { confirm } = await import("@inquirer/prompts");
166296
const answer = await confirm({
167-
message: "Are you sure you want to start the deployment process?",
168-
default: false,
297+
message: "Do you want to proceed with the deployment?",
298+
default: true,
169299
});
170300

171301
if (!answer) {
@@ -174,7 +304,20 @@ async function deploy() {
174304
}
175305
}
176306

177-
await Promise.all(jsFiles.map(uploadFile));
307+
const uploadPromises = changedFiles.map((f) => {
308+
if (!f.content) {
309+
// Should not happen given checkFileStatus logic for changed/new
310+
return Promise.resolve({
311+
filename: f.filename,
312+
status: "error" as const,
313+
size: 0,
314+
});
315+
}
316+
return uploadFile(f.filename, f.content, f.size);
317+
});
318+
319+
const results = await Promise.all(uploadPromises);
320+
const uploaded = results.filter((r) => r.status === "uploaded");
178321

179322
if (options.dryRun) {
180323
console.log(
@@ -183,14 +326,37 @@ async function deploy() {
183326
} else {
184327
console.log(
185328
chalk.green(
186-
`\n✨ Deployment process completed! (${jsFiles.length} files processed)`
329+
`\n✨ Deployment process completed! (${uploaded.length} files updated)`
187330
)
188331
);
332+
333+
if (uploaded.length > 0) {
334+
console.log(chalk.dim("\n🧹 Purging Pull Zone cache..."));
335+
await purgePullZoneCache();
336+
337+
if (options.skipNotification) {
338+
console.log(
339+
chalk.gray("🔕 Skipping Discord notification (--skip-notification)")
340+
);
341+
} else {
342+
await sendDiscordNotification(uploaded, options.message);
343+
}
344+
}
189345
}
190346
} catch (error) {
191347
console.error(chalk.red("❌ Deployment failed:"), error);
192348
process.exit(1);
193349
}
194350
}
195351

196-
deploy();
352+
if (options.purge) {
353+
console.log(chalk.bold("\n🧹 Purging Pull Zone cache..."));
354+
purgePullZoneCache().then(() => {
355+
process.exit(0);
356+
}).catch((error) => {
357+
console.error(chalk.red("❌ Purge failed:"), error);
358+
process.exit(1);
359+
});
360+
} else {
361+
deploy();
362+
}

0 commit comments

Comments
 (0)