mirror of
https://github.com/samber/awesome-prometheus-alerts.git
synced 2026-06-24 18:36:59 +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;
|
class?: string;
|
||||||
withAttribution?: boolean;
|
withAttribution?: boolean;
|
||||||
nudge?: 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 btnId = `copy-btn-${targetId}`;
|
||||||
const nudgeId = `star-nudge-${btnId}`;
|
const nudgeId = `star-nudge-${btnId}`;
|
||||||
---
|
---
|
||||||
|
|
@ -65,7 +66,7 @@ const nudgeId = `star-nudge-${btnId}`;
|
||||||
</button>
|
</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);
|
const btn = document.getElementById(btnId);
|
||||||
if (!(btn instanceof HTMLButtonElement)) return;
|
if (!(btn instanceof HTMLButtonElement)) return;
|
||||||
if (btn.dataset.copyBound === 'true') 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'));
|
window.dispatchEvent(new CustomEvent('copy-success'));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@ interface Props {
|
||||||
exporter: Exporter;
|
exporter: Exporter;
|
||||||
service: Service;
|
service: Service;
|
||||||
groupIndex: number;
|
groupIndex: number;
|
||||||
|
groupName: string;
|
||||||
|
groupSlug: string;
|
||||||
serviceIndex: number;
|
serviceIndex: number;
|
||||||
|
serviceSlug: string;
|
||||||
exporterIndex: number;
|
exporterIndex: number;
|
||||||
showExporterNumber: boolean;
|
showExporterNumber: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +20,10 @@ const {
|
||||||
exporter,
|
exporter,
|
||||||
service,
|
service,
|
||||||
groupIndex,
|
groupIndex,
|
||||||
|
groupName,
|
||||||
|
groupSlug,
|
||||||
serviceIndex,
|
serviceIndex,
|
||||||
|
serviceSlug,
|
||||||
exporterIndex,
|
exporterIndex,
|
||||||
showExporterNumber,
|
showExporterNumber,
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
@ -68,7 +74,26 @@ const exporterPrefix = showExporterNumber
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -88,7 +113,23 @@ const exporterPrefix = showExporterNumber
|
||||||
</svg>
|
</svg>
|
||||||
<pre id={wgetId} class="text-xs font-mono text-slate-600 dark:text-slate-300 overflow-x-auto whitespace-pre">{wgetCommand}</pre>
|
<pre id={wgetId} class="text-xs font-mono text-slate-600 dark:text-slate-300 overflow-x-auto whitespace-pre">{wgetCommand}</pre>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -114,6 +155,26 @@ const exporterPrefix = showExporterNumber
|
||||||
rule={rule}
|
rule={rule}
|
||||||
anchorId={anchorId}
|
anchorId={anchorId}
|
||||||
ruleNumber={ruleNumber}
|
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;
|
rule: Rule;
|
||||||
anchorId: string;
|
anchorId: string;
|
||||||
ruleNumber: 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 yamlContent = formatRuleAsYaml(rule);
|
||||||
const codeId = `code-${anchorId}`;
|
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" />
|
<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>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<CopyButton targetId={codeId} variant="icon" nudge={true} />
|
<CopyButton targetId={codeId} variant="icon" nudge={true} copyData={copyData} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,5 +98,12 @@ const canonical = canonicalUrl ?? `${SITE_ORIGIN}${base}${Astro.url.pathname.rep
|
||||||
|
|
||||||
<Footer base={base} />
|
<Footer base={base} />
|
||||||
<StarToast />
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,10 @@ const jsonLd = {
|
||||||
exporter={exporter}
|
exporter={exporter}
|
||||||
service={service}
|
service={service}
|
||||||
groupIndex={groupIndex}
|
groupIndex={groupIndex}
|
||||||
|
groupName={group.name}
|
||||||
|
groupSlug={groupSlug}
|
||||||
serviceIndex={serviceIndex}
|
serviceIndex={serviceIndex}
|
||||||
|
serviceSlug={serviceSlug}
|
||||||
exporterIndex={expIdx + 1}
|
exporterIndex={expIdx + 1}
|
||||||
showExporterNumber={service.exporters.length > 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