@birdie-so/snippet
Version:
Helper for integrating the Birdie screen recording snippet into modern JavaScript apps. Requires a Birdie account.
301 lines (240 loc) • 9.66 kB
JavaScript
(function () {
try {
if (typeof window === 'undefined' || typeof document === 'undefined') return;
var params = new URLSearchParams(window.location.search);
var recordingUrl = params.get('birdie-recording');
if (!recordingUrl) return;
try {
var parsed = new URL(decodeURIComponent(recordingUrl));
if (parsed.protocol !== 'https:') return;
if (parsed.searchParams.get('birdie-embed') !== '1') return;
} catch (e) {
return;
}
if (!document.getElementById('birdie-recording-preload-style')) {
var style = document.createElement('style');
style.id = 'birdie-recording-preload-style';
style.textContent = `
html, body {
overflow: hidden !important;
}
#birdie-recording-preload-overlay {
position: fixed;
inset: 0;
background: #fff;
z-index: 2147483647;
pointer-events: auto;
}
`;
(document.head || document.documentElement).appendChild(style);
}
if (!document.getElementById('birdie-recording-preload-overlay')) {
var overlay = document.createElement('div');
overlay.id = 'birdie-recording-preload-overlay';
if (document.documentElement) {
document.documentElement.appendChild(overlay);
}
}
} catch (e) { }
})();
let _readyCallbacks = [];
function ensureBirdieSettings() {
if (typeof window === 'undefined') return {};
if (!window.birdieSettings) {
window.birdieSettings = {};
}
return window.birdieSettings;
}
function normalizeContact(contact, otherSettings = {}) {
if (contact && typeof contact === 'object') {
return contact;
}
const legacyContact = {};
if (typeof otherSettings.contact_id !== 'undefined') legacyContact.id = otherSettings.contact_id;
if (typeof otherSettings.contact_name !== 'undefined') legacyContact.name = otherSettings.contact_name;
if (typeof otherSettings.contact_email !== 'undefined') legacyContact.email = otherSettings.contact_email;
if (typeof otherSettings.contact_phone !== 'undefined') legacyContact.phone = otherSettings.contact_phone;
return Object.keys(legacyContact).length ? legacyContact : undefined;
}
function applyBirdieRuntimeUpdate({ metadata, contact } = {}) {
if (typeof window === 'undefined') return;
const settings = ensureBirdieSettings();
if (typeof metadata !== 'undefined') {
settings.metadata = metadata;
}
if (typeof contact !== 'undefined') {
settings.contact = contact;
}
if (!window.birdie) return;
if (typeof metadata !== 'undefined') {
if (typeof window.birdie.setMetadata === 'function') {
window.birdie.setMetadata(metadata);
} else {
window.birdie.metadata = metadata;
}
}
if (typeof contact !== 'undefined') {
if (typeof window.birdie.setContact === 'function') {
window.birdie.setContact(contact);
} else {
window.birdie.contact = contact;
}
}
}
function injectBirdieScript(clientId) {
const existing = document.getElementById('birdie-snippet');
if (existing) return existing;
const embed_path = clientId == 'e4bf56a2' ? 'embed_birdie' : 'embed';
// Load the loader straight from CloudFront. app.birdie.so/widget/embed/* is
// 302-redirected to cf.birdie.so by the ALB, so hitting cf directly skips
// that hop — and makes the preconnect/preload below target the host the
// script is actually fetched from (previously they warmed/preloaded
// app.birdie.so, i.e. only the redirect, not the script).
const SNIPPET_HOST = 'https://cf.birdie.so';
const scriptUrl = `${SNIPPET_HOST}/widget/${embed_path}/${clientId}?_bh=${encodeURIComponent(window.location.hostname)}`;
if (document.head) {
if (!document.querySelector('link[data-birdie-preconnect]')) {
const preconnect = document.createElement('link');
preconnect.rel = 'preconnect';
preconnect.href = SNIPPET_HOST;
preconnect.crossOrigin = 'anonymous';
preconnect.setAttribute('data-birdie-preconnect', 'true');
document.head.appendChild(preconnect);
}
if (!document.querySelector('link[data-birdie-preload]')) {
const preload = document.createElement('link');
preload.rel = 'preload';
preload.as = 'script';
preload.href = scriptUrl;
preload.setAttribute('data-birdie-preload', 'true');
document.head.appendChild(preload);
}
}
const script = document.createElement('script');
script.src = scriptUrl;
script.async = true;
script.id = 'birdie-snippet';
script.referrerPolicy = 'origin';
const firstScript = document.scripts?.[0];
if (firstScript?.parentNode) {
firstScript.parentNode.insertBefore(script, firstScript);
} else if (document.head) {
document.head.appendChild(script);
} else {
document.documentElement.appendChild(script);
}
return script;
}
export function initBirdie({ clientId, metadata, contact, onReady, singleHandlerPerEvent = true, ...otherSettings }) {
if (!clientId) {
console.error('[Birdie] Missing required clientId in initBirdie()');
return;
}
if (typeof window === 'undefined') return;
// Always predefine birdieSettings first (or reuse if already present)
// if (!window.birdieSettings) {
// window.birdieSettings = {};
// }
// Assign metadata and other settings first
// window.birdieSettings.metadata = metadata;
const settings = ensureBirdieSettings();
const normalizedContact = normalizeContact(contact, otherSettings);
settings.metadata = metadata;
if (typeof normalizedContact !== 'undefined') {
settings.contact = normalizedContact;
}
Object.assign(window.birdieSettings, otherSettings);
window.birdieSettings.onBirdieReady = () => {
try {
installBirdieGuards({ singleHandlerPerEvent });
} catch (e) {
console.error('[Birdie] guard install error', e);
}
_readyCallbacks.forEach(cb => { try { cb(window.birdie); } catch (e) { console.error('[Birdie] onReady error', e); } });
_readyCallbacks = [];
};
// Queue the onReady handler
if (onReady) {
registerBirdieReadyCallback(onReady);
}
// Inject script if not already
// if (!document.getElementById('birdie-snippet')) {
// const script = document.createElement('script');
// const embed_path = clientId == 'e4bf56a2' ? 'embed_birdie' : 'embed'
// script.src = `https://app.birdie.so/widget/${embed_path}/${clientId}`;
// script.async = true;
// script.id = 'birdie-snippet';
// script.referrerPolicy = "origin";
// document.body.appendChild(script);
// }
injectBirdieScript(clientId);
// if (!document.getElementById('birdie-snippet')) {
// const script = document.createElement('script');
// script.src = `https://birdie.eu.ngrok.io/snippet/index.local.min.js`;
// script.async = true;
// script.id = 'birdie-snippet';
// document.body.appendChild(script);
// }
}
function registerBirdieReadyCallback(callback) {
if (typeof window === 'undefined') return;
if (window.birdie) {
callback(window.birdie);
} else {
_readyCallbacks.push(callback);
}
}
export function getBirdieInstance(onReady) {
if (typeof window === 'undefined') return null;
if (window.birdie) {
onReady?.(window.birdie);
return window.birdie;
} else {
registerBirdieReadyCallback(onReady);
return null;
}
}
export function setBirdieMetadata(metadata) {
applyBirdieRuntimeUpdate({ metadata });
}
export function setBirdieContact(contact) {
applyBirdieRuntimeUpdate({ contact });
}
export function updateBirdie({ metadata, contact } = {}) {
applyBirdieRuntimeUpdate({ metadata, contact });
}
// ---- guards & helpers ----
function installBirdieGuards({ singleHandlerPerEvent }) {
const b = window.birdie;
if (!b || b.__pkgGuardsInstalled) return;
// Keep originals
const originalOn = b.on?.bind(b);
const originalOff = b.off?.bind(b);
// Registry of the single active handler per event (for dedupe)
const singletons = new Map();
// Add onOnce
b.onOnce = (event, cb) => {
const wrapper = (data) => {
try { cb(data); } finally { b.off?.(event, wrapper); }
};
b.on(event, wrapper);
};
if (singleHandlerPerEvent && originalOn && originalOff) {
b.on = (event, cb) => {
// If there was a previous handler for this event installed via this layer, remove it
const prev = singletons.get(event);
if (prev) {
try { originalOff(event, prev); } catch { }
}
singletons.set(event, cb);
return originalOn(event, cb);
};
b.off = (event, cb) => {
// Clean our registry if the same cb is removed
const prev = singletons.get(event);
if (prev === cb) singletons.delete(event);
return originalOff(event, cb);
};
}
b.__pkgGuardsInstalled = true;
}