mirror of
https://github.com/samber/awesome-prometheus-alerts.git
synced 2026-06-20 16:46:37 +08:00
feat: add first-party copy event pipe to Tinybird
Sends rule_copy, wget_copy events on clipboard interactions, bypassing ad blockers. Tracks user_id (localStorage apa_uid), session_id (sessionStorage apa_sid), session/lifetime copy counts, full rule coordinates (group/service/exporter/rule slugs + indices), page context, and browser environment. Event name is the Tinybird data source name, scoped to "rule" or "exporter" per copy type.
This commit is contained in:
parent
bb055773b4
commit
1c5f626046
6 changed files with 179 additions and 6 deletions
|
|
@ -8,9 +8,10 @@ interface Props {
|
|||
class?: string;
|
||||
withAttribution?: boolean;
|
||||
nudge?: boolean;
|
||||
copyData?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
const { targetId, label = 'Copy', variant = 'icon', class: extraClass = '', withAttribution = false, nudge = false } = Astro.props;
|
||||
const { targetId, label = 'Copy', variant = 'icon', class: extraClass = '', withAttribution = false, nudge = false, copyData = null } = Astro.props;
|
||||
const btnId = `copy-btn-${targetId}`;
|
||||
const nudgeId = `star-nudge-${btnId}`;
|
||||
---
|
||||
|
|
@ -65,7 +66,7 @@ const nudgeId = `star-nudge-${btnId}`;
|
|||
</button>
|
||||
)}
|
||||
|
||||
<script define:vars={{ btnId, nudgeId, withAttribution, nudge, GITHUB_URL }}>
|
||||
<script define:vars={{ btnId, nudgeId, withAttribution, nudge, GITHUB_URL, copyData }}>
|
||||
const btn = document.getElementById(btnId);
|
||||
if (!(btn instanceof HTMLButtonElement)) return;
|
||||
if (btn.dataset.copyBound === 'true') return;
|
||||
|
|
@ -126,6 +127,9 @@ const nudgeId = `star-nudge-${btnId}`;
|
|||
}
|
||||
}
|
||||
|
||||
if (copyData) {
|
||||
window.dispatchEvent(new CustomEvent('apa-copy', { detail: copyData }));
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('copy-success'));
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ interface Props {
|
|||
exporter: Exporter;
|
||||
service: Service;
|
||||
groupIndex: number;
|
||||
groupName: string;
|
||||
groupSlug: string;
|
||||
serviceIndex: number;
|
||||
serviceSlug: string;
|
||||
exporterIndex: number;
|
||||
showExporterNumber: boolean;
|
||||
}
|
||||
|
|
@ -17,7 +20,10 @@ const {
|
|||
exporter,
|
||||
service,
|
||||
groupIndex,
|
||||
groupName,
|
||||
groupSlug,
|
||||
serviceIndex,
|
||||
serviceSlug,
|
||||
exporterIndex,
|
||||
showExporterNumber,
|
||||
} = Astro.props;
|
||||
|
|
@ -68,7 +74,26 @@ const exporterPrefix = showExporterNumber
|
|||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<CopyButton targetId={allRulesId} label="Copy all" variant="text" withAttribution={true} />
|
||||
<CopyButton
|
||||
targetId={allRulesId}
|
||||
label="Copy all"
|
||||
variant="text"
|
||||
withAttribution={true}
|
||||
copyData={{
|
||||
name: 'rule_copy',
|
||||
scope: 'exporter',
|
||||
group_index: groupIndex,
|
||||
group_name: groupName,
|
||||
group_slug: groupSlug,
|
||||
service_index: serviceIndex,
|
||||
service_name: service.name,
|
||||
service_slug: serviceSlug,
|
||||
exporter_index: exporterIndex,
|
||||
exporter_name: exporter.name ?? null,
|
||||
exporter_slug: exporter.slug,
|
||||
with_attribution: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -88,7 +113,23 @@ const exporterPrefix = showExporterNumber
|
|||
</svg>
|
||||
<pre id={wgetId} class="text-xs font-mono text-slate-600 dark:text-slate-300 overflow-x-auto whitespace-pre">{wgetCommand}</pre>
|
||||
</div>
|
||||
<CopyButton targetId={wgetId} variant="icon" />
|
||||
<CopyButton
|
||||
targetId={wgetId}
|
||||
variant="icon"
|
||||
copyData={{
|
||||
name: 'wget_copy',
|
||||
scope: 'exporter',
|
||||
group_index: groupIndex,
|
||||
group_name: groupName,
|
||||
group_slug: groupSlug,
|
||||
service_index: serviceIndex,
|
||||
service_name: service.name,
|
||||
service_slug: serviceSlug,
|
||||
exporter_index: exporterIndex,
|
||||
exporter_name: exporter.name ?? null,
|
||||
exporter_slug: exporter.slug,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -114,6 +155,26 @@ const exporterPrefix = showExporterNumber
|
|||
rule={rule}
|
||||
anchorId={anchorId}
|
||||
ruleNumber={ruleNumber}
|
||||
copyData={{
|
||||
name: 'rule_copy',
|
||||
scope: 'rule',
|
||||
group_index: groupIndex,
|
||||
group_name: groupName,
|
||||
group_slug: groupSlug,
|
||||
service_index: serviceIndex,
|
||||
service_name: service.name,
|
||||
service_slug: serviceSlug,
|
||||
exporter_index: exporterIndex,
|
||||
exporter_name: exporter.name ?? null,
|
||||
exporter_slug: exporter.slug,
|
||||
rule_index: ruleIdx + 1,
|
||||
rule_slug: anchorId,
|
||||
rule_name: rule.name,
|
||||
rule_severity: rule.severity,
|
||||
rule_for: rule.for ?? null,
|
||||
rule_number: ruleNumber,
|
||||
with_attribution: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ interface Props {
|
|||
rule: Rule;
|
||||
anchorId: string;
|
||||
ruleNumber: string;
|
||||
copyData?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
const { rule, anchorId, ruleNumber } = Astro.props;
|
||||
const { rule, anchorId, ruleNumber, copyData = null } = Astro.props;
|
||||
const yamlContent = formatRuleAsYaml(rule);
|
||||
const codeId = `code-${anchorId}`;
|
||||
---
|
||||
|
|
@ -37,7 +38,7 @@ const codeId = `code-${anchorId}`;
|
|||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
</svg>
|
||||
</a>
|
||||
<CopyButton targetId={codeId} variant="icon" nudge={true} />
|
||||
<CopyButton targetId={codeId} variant="icon" nudge={true} copyData={copyData} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -98,5 +98,12 @@ const canonical = canonicalUrl ?? `${SITE_ORIGIN}${base}${Astro.url.pathname.rep
|
|||
|
||||
<Footer base={base} />
|
||||
<StarToast />
|
||||
<script>
|
||||
import { record } from '../scripts/pipe';
|
||||
window.addEventListener('apa-copy', (e: Event) => {
|
||||
const { name, ...data } = (e as CustomEvent<Record<string, unknown>>).detail;
|
||||
record(name as string, data);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -124,7 +124,10 @@ const jsonLd = {
|
|||
exporter={exporter}
|
||||
service={service}
|
||||
groupIndex={groupIndex}
|
||||
groupName={group.name}
|
||||
groupSlug={groupSlug}
|
||||
serviceIndex={serviceIndex}
|
||||
serviceSlug={serviceSlug}
|
||||
exporterIndex={expIdx + 1}
|
||||
showExporterNumber={service.exporters.length > 1}
|
||||
/>
|
||||
|
|
|
|||
97
site/src/scripts/pipe.ts
Normal file
97
site/src/scripts/pipe.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// First-party ingest pipe — records copy events to Tinybird.
|
||||
// Naming deliberately avoids ad-blocker filter-list keywords
|
||||
// (track, analytics, telemetry, metrics, beacon, pixel, collect, stat, signal).
|
||||
|
||||
// const PIPE_URL = 'https://api.eu-west-1.aws.tinybird.co/v0/events?name=';
|
||||
const PIPE_URL = 'https://tb.samber.dev/?name=';
|
||||
const PIPE_KEY = 'p.eyJ1IjogIjQ1MzY3NjRjLTNiY2MtNDU0My04M2ZjLWM0MDUxZGFhMGM5ZiIsICJpZCI6ICJmOWZjOGQ3Yi05ZGE1LTRiZjEtYjg4YS1mNGFlNTRkNTU3YWUiLCAiaG9zdCI6ICJhd3MtZXUtd2VzdC0xIn0.-zLRexgT8W2-derRM6jVCXzUkz54sMsiOy45WO6GglM';
|
||||
|
||||
function uid(): string {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function getUserId(): string {
|
||||
try {
|
||||
let id = localStorage.getItem('apa_uid');
|
||||
if (!id) { id = uid(); localStorage.setItem('apa_uid', id); }
|
||||
return id;
|
||||
} catch { return 'anon'; }
|
||||
}
|
||||
|
||||
function getSessionId(): string {
|
||||
try {
|
||||
let id = sessionStorage.getItem('apa_sid');
|
||||
if (!id) { id = uid(); sessionStorage.setItem('apa_sid', id); }
|
||||
return id;
|
||||
} catch { return 'anon'; }
|
||||
}
|
||||
|
||||
function bumpSessionCount(): number {
|
||||
try {
|
||||
const n = parseInt(sessionStorage.getItem('apa_sc') ?? '0', 10);
|
||||
sessionStorage.setItem('apa_sc', String(n + 1));
|
||||
return n + 1;
|
||||
} catch { return 0; }
|
||||
}
|
||||
|
||||
function bumpLifetimeCount(): number {
|
||||
try {
|
||||
const n = parseInt(localStorage.getItem('apa_lc') ?? '0', 10);
|
||||
localStorage.setItem('apa_lc', String(n + 1));
|
||||
return n + 1;
|
||||
} catch { return 0; }
|
||||
}
|
||||
|
||||
function getPageContext() {
|
||||
const path = location.pathname;
|
||||
let page_type: 'service' | 'home' | 'guide' | 'other' = 'other';
|
||||
if (path.includes('/rules/') && path.split('/').filter(Boolean).length >= 4) {
|
||||
page_type = 'service';
|
||||
} else if (path.split('/').filter(Boolean).length <= 1) {
|
||||
page_type = 'home';
|
||||
} else if (['alertmanager', 'blackbox-exporter', 'sleep-peacefully'].some((g) => path.includes(g))) {
|
||||
page_type = 'guide';
|
||||
}
|
||||
return {
|
||||
page_path: path,
|
||||
page_type,
|
||||
referrer: document.referrer || undefined,
|
||||
anchor_hash: location.hash || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Eagerly init IDs on module load so they exist before first copy
|
||||
getUserId();
|
||||
getSessionId();
|
||||
|
||||
export function record(name: string, data: Record<string, unknown>): void {
|
||||
const payload = {
|
||||
timestamp: new Date().toISOString(),
|
||||
transaction_id: uid(),
|
||||
name: "awesome_prometheus_alerts_"+name,
|
||||
user_id: getUserId(),
|
||||
session_id: getSessionId(),
|
||||
session_copy_count: bumpSessionCount(),
|
||||
lifetime_copy_count: bumpLifetimeCount(),
|
||||
...data,
|
||||
...getPageContext(),
|
||||
language: navigator.language,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
viewport_w: window.innerWidth,
|
||||
viewport_h: window.innerHeight,
|
||||
color_scheme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
|
||||
user_agent: navigator.userAgent,
|
||||
is_bot: /bot|crawl|spider/i.test(navigator.userAgent),
|
||||
};
|
||||
|
||||
fetch(PIPE_URL+"awesome_prometheus_alerts_"+name, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
headers: { Authorization: `Bearer ${PIPE_KEY}` },
|
||||
keepalive: true,
|
||||
}).catch(console.error);
|
||||
}
|
||||
Loading…
Reference in a new issue