One file. ES module. One HTTP call on install.
6,950 bytes uncompressed, MIT licensed. The whole source is at the bottom of this page.
Get it
Direct download
/sdk/ef-sdk.js · right-click → save as
GitHub (source, issues, PRs)
github.com/USSupportLLC/ExtensionFeedback
Or via curl
curl -O https://extensionfeedback.com/sdk/ef-sdk.js
Cached CDN URL
https://extensionfeedback.com/sdk/ef-sdk.js
API
ExtensionFeedback.init(options)
Call once during background-service-worker startup. Idempotent — safe to call on every wake. Both pages are independently optional.
ExtensionFeedback.init({
apiKey: string, // required. "ef_live_..." from your dashboard
install: boolean = false, // optional. open install page on first install
uninstall: boolean = true, // optional. wire chrome.runtime.setUninstallURL
});
Pass install: false (or omit) to skip the install page entirely. Pass uninstall: false to skip wiring the uninstall URL — Chrome's default uninstall behavior takes over.
ExtensionFeedback.openFeedback()
Opens the install page in a new tab on demand — useful for a "Send feedback" button in your popup.
document.querySelector("#feedback-btn")
.addEventListener("click", () =>
ExtensionFeedback.openFeedback()
);
What it actually does
- On first init, makes one HTTPS POST to
https://extensionfeedback.com/api/v1/installsending{ ext, key, iid }; receives a 64-char per-install signing secret. The secret is cached inchrome.storage.localunder__ef_install_secret— no further network calls until storage is cleared. - Generates a 128-bit random
installIdon first run, stored inchrome.storage.localunder key__ef_install_id. - Calls
chrome.runtime.setUninstallURL(https://extensionfeedback.com/u/<extId>?key=…&iid=…&sig=…). Thesigis HMAC-SHA256 overextId|iid|version|localeusing the per-install secret — lets the server reject forged URLs from anyone who only has the apiKey. - On
chrome.runtime.onInstalledwith reasoninstall, openshttps://extensionfeedback.com/i/<extId>?…&sig=…ifinstall: true.
The whole thing, inline
Exactly what you'll get from /sdk/ef-sdk.js. Read it, audit it, modify it — it's MIT.
/**
* ExtensionFeedback SDK — install/uninstall feedback for Chrome extensions.
*
* Drop this file next to your background.js, then:
*
* import { ExtensionFeedback } from "./ef-sdk.js";
* ExtensionFeedback.init({
* apiKey: "ef_live_...",
* install: true, // optional; default false. Open install page on first install.
* uninstall: true, // optional; default true. Wire uninstall URL.
* });
*
* Both pages are independently optional. Pass install: false (or omit) to skip the
* install page; pass uninstall: false to skip wiring the uninstall URL.
*
* Behavior:
* - On first init, makes ONE HTTPS POST to {HOST}/api/v1/install to register
* this install and obtain a per-install signing secret. The secret is
* stored in chrome.storage.local under __ef_install_secret. Secrets are
* unique per install_id and used to HMAC-sign all subsequent feedback URLs.
* - On chrome.runtime.onInstalled with reason "install", opens the install page
* (only if init({ install: true })).
* - On init, calls chrome.runtime.setUninstallURL() so Chrome opens the uninstall
* page automatically when the user removes your extension (only if
* uninstall !== false; default true).
* - A 128-bit random installId is stored in chrome.storage.local under
* __ef_install_id and reused across both pages.
*
* Manifest requirements: "storage" permission only. The fetch() to
* extensionfeedback.com from a service worker works without host_permissions
* because the server returns CORS headers.
*
* License: MIT.
*/
const HOST = "https://extensionfeedback.com";
const STORAGE_KEY = "__ef_install_id";
const SECRET_STORAGE_KEY = "__ef_install_secret";
let _apiKey = null;
let _installEnabled = false;
let _uninstallEnabled = true;
let _ready = null;
async function getOrCreateInstallId() {
const stored = await chrome.storage.local.get(STORAGE_KEY);
if (stored && typeof stored[STORAGE_KEY] === "string" && stored[STORAGE_KEY].length === 32) {
return stored[STORAGE_KEY];
}
const buf = new Uint8Array(16);
crypto.getRandomValues(buf);
const id = Array.from(buf, b => b.toString(16).padStart(2, "0")).join("");
await chrome.storage.local.set({ [STORAGE_KEY]: id });
return id;
}
async function getOrFetchInstallSecret(extId, installId) {
const stored = await chrome.storage.local.get(SECRET_STORAGE_KEY);
if (stored && typeof stored[SECRET_STORAGE_KEY] === "string" && stored[SECRET_STORAGE_KEY].length === 64) {
return stored[SECRET_STORAGE_KEY];
}
// Register this install with the server. ONE HTTP call, ever.
const resp = await fetch(`${HOST}/api/v1/install`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ext: extId, key: _apiKey, iid: installId }),
});
if (!resp.ok) {
throw new Error(`[ExtensionFeedback] register failed: HTTP ${resp.status}`);
}
const data = await resp.json();
if (!data || typeof data.install_secret !== "string" || data.install_secret.length !== 64) {
throw new Error("[ExtensionFeedback] register returned invalid secret");
}
await chrome.storage.local.set({ [SECRET_STORAGE_KEY]: data.install_secret });
return data.install_secret;
}
async function hmacSha256Hex(secretHex, msg) {
const enc = new TextEncoder();
const keyBytes = enc.encode(secretHex);
const msgBytes = enc.encode(msg);
const cryptoKey = await crypto.subtle.importKey(
"raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
);
const sig = await crypto.subtle.sign("HMAC", cryptoKey, msgBytes);
return Array.from(new Uint8Array(sig), b => b.toString(16).padStart(2, "0")).join("");
}
async function buildUrl(prefix) {
const extId = chrome.runtime.id;
const v = chrome.runtime.getManifest().version || "";
const iid = await getOrCreateInstallId();
const l = (chrome.i18n && chrome.i18n.getUILanguage) ? chrome.i18n.getUILanguage() : "en";
const installSecret = await getOrFetchInstallSecret(extId, iid);
const payload = `${extId}|${iid}|${v}|${l}`;
const sig = await hmacSha256Hex(installSecret, payload);
const qs = new URLSearchParams({ key: _apiKey, v, iid, l, sig });
return `${HOST}${prefix}${extId}?${qs.toString()}`;
}
async function setUninstallUrl() {
const url = await buildUrl("/u/");
if (chrome.runtime.setUninstallURL) {
chrome.runtime.setUninstallURL(url);
}
}
export const ExtensionFeedback = {
/**
* Initialize. Call once on service-worker startup. Idempotent.
*
* @param {object} options
* @param {string} options.apiKey "ef_live_..." from your dashboard
* @param {boolean} [options.install=false] open install page on first install
* @param {boolean} [options.uninstall=true] wire uninstall URL (Chrome opens on uninstall)
*/
init(options) {
if (!options || typeof options.apiKey !== "string" || !options.apiKey.startsWith("ef_live_")) {
console.warn("[ExtensionFeedback] init() requires a valid apiKey starting with ef_live_");
return;
}
_apiKey = options.apiKey;
_installEnabled = !!options.install;
_uninstallEnabled = options.uninstall !== false; // default true
// Wire onInstalled handler — only fires on actual install/update events.
if (chrome.runtime && chrome.runtime.onInstalled) {
chrome.runtime.onInstalled.addListener(async (details) => {
if (details.reason === "install" && _installEnabled) {
try {
const url = await buildUrl("/i/");
chrome.tabs.create({ url });
} catch (e) {
console.warn("[ExtensionFeedback] could not open install page:", e);
}
}
});
}
// Set uninstall URL now (idempotent — Chrome stores the latest value).
// Triggers the install-secret fetch on first run if missing.
if (_uninstallEnabled) {
_ready = setUninstallUrl().catch((e) => {
console.warn("[ExtensionFeedback] could not set uninstall URL:", e);
});
}
},
/**
* Open the install page in a new tab on demand. Useful for a "Send feedback"
* button in your popup. Returns the chrome.tabs.create() promise.
*/
async openFeedback() {
if (!_apiKey) {
console.warn("[ExtensionFeedback] openFeedback() called before init()");
return;
}
const url = await buildUrl("/i/");
return chrome.tabs.create({ url });
},
/** Internal — exposed only for tests. */
_getInstallId: getOrCreateInstallId,
};