1
0
mirror of https://github.com/XFox111/TabsAsideExtension.git synced 2026-04-22 07:58:01 +03:00

feat: ga4 analytics #117

This commit is contained in:
2025-05-05 19:25:25 +03:00
parent b6be86aac9
commit 5d4a59153a
25 changed files with 135 additions and 11 deletions
+4
View File
@@ -47,6 +47,10 @@ jobs:
steps:
- uses: actions/checkout@main
- run: |
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
- run: yarn install
- run: yarn zip -b ${{ matrix.target }}
+4
View File
@@ -39,6 +39,10 @@ jobs:
steps:
- uses: actions/checkout@main
- run: |
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
- run: yarn install
- run: yarn zip -b ${{ matrix.target }}
+2
View File
@@ -26,3 +26,5 @@ web-ext.config.ts
*.sw?
web-ext.config.js
.env*
+24
View File
@@ -0,0 +1,24 @@
import { googleAnalytics4 } from "@wxt-dev/analytics/providers/google-analytics-4";
import { WxtAppConfig } from "wxt/sandbox";
import { userPropertiesStorage } from "./features/analytics";
export default defineAppConfig({
analytics:
{
debug: import.meta.env.DEV,
enabled: storage.defineItem("local:analytics", {
fallback: true
}),
userId: storage.defineItem("local:userId", {
init: () => crypto.randomUUID()
}),
userProperties: userPropertiesStorage,
providers:
[
googleAnalytics4({
apiSecret: import.meta.env.WXT_GA4_API_SECRET,
measurementId: import.meta.env.WXT_GA4_MEASUREMENT_ID
})
]
}
} as WxtAppConfig);
+4 -3
View File
@@ -1,3 +1,4 @@
import { trackError } from "@/features/analytics";
import { collectionCount, getCollections, saveCollections } from "@/features/collectionStorage";
import { migrateStorage } from "@/features/migration";
import { showWelcomeDialog } from "@/features/v3welcome/utils/showWelcomeDialog";
@@ -30,6 +31,7 @@ export default defineBackground(() =>
browser.runtime.onInstalled.addListener(async ({ reason, previousVersion }) =>
{
logger("onInstalled", reason, previousVersion);
analytics.track("extension_installed", { reason, previousVersion: previousVersion ?? "none" });
const previousMajor: number = previousVersion ? parseInt(previousVersion.split(".")[0]) : 0;
@@ -88,11 +90,9 @@ export default defineBackground(() =>
preview: graphicsCache[tab.url]?.preview,
icon: graphicsCache[tab.url]?.icon
};
logger("Captured tab", tab.url);
}
}
catch (ex) { logger(ex); }
catch { }
};
setInterval(() =>
@@ -374,5 +374,6 @@ export default defineBackground(() =>
catch (ex)
{
console.error(ex);
trackError("background_error", ex as Error);
}
});
+2
View File
@@ -14,6 +14,8 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
</App>
);
analytics.page("options_page");
function OptionsPage(): React.ReactElement
{
const [selection, setSelection] = useState<SelectionType>("general");
@@ -26,6 +26,11 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
const handleSave = () =>
{
if (props.type === "collection" ? props.collection !== null : props.group !== null)
analytics.track("item_edited", { type: props.type });
else
analytics.track("item_created", { type: props.type });
if (props.type === "collection")
props.onSave({
type: "collection",
@@ -64,6 +64,8 @@ export default function CollectionListView(): ReactElement
updateCollections(result);
if (sortMode !== "custom")
setSortMode("custom");
analytics.track("used_drag_and_drop");
}
};
@@ -27,6 +27,11 @@ export default function CtaMessage(props: MessageBarProps): ReactElement
{
await ctaCounter.setValue(counter);
setCounter(counter);
if (counter === -1)
analytics.track("bmc_clicked");
else
analytics.track("cta_dismissed");
};
if (counter < 50)
@@ -36,7 +41,7 @@ export default function CtaMessage(props: MessageBarProps): ReactElement
<MessageBar layout="multiline" icon={ <HeartFilled color="red" /> } { ...props }>
<MessageBarBody>
<MessageBarTitle>{ i18n.t("cta_message.title") }</MessageBarTitle>
{ i18n.t("cta_message.message") } <Link { ...extLink(storeLink) }>{ i18n.t("cta_message.feedback") }</Link>
{ i18n.t("cta_message.message") } <Link { ...extLink(storeLink) } onClick={ () => analytics.track("feedback_clicked") }>{ i18n.t("cta_message.feedback") }</Link>
</MessageBarBody>
<MessageBarActions
containerAction={
@@ -41,10 +41,10 @@ export default function MoreButton(): ReactElement
<fui.MenuDivider />
<fui.MenuItemLink icon={ <BmcIcon /> } { ...extLink(buyMeACoffeeLink) }>
<fui.MenuItemLink icon={ <BmcIcon /> } { ...extLink(buyMeACoffeeLink) } onClick={ () => analytics.track("feedback_clicked") }>
{ i18n.t("common.cta.sponsor") }
</fui.MenuItemLink>
<fui.MenuItemLink icon={ <FeedbackIcon /> } { ...extLink(storeLink) } >
<fui.MenuItemLink icon={ <FeedbackIcon /> } { ...extLink(storeLink) } onClick={ () => analytics.track("bmc_clicked") }>
{ i18n.t("common.cta.feedback") }
</fui.MenuItemLink>
<fui.MenuItemLink icon={ <LearnIcon /> } { ...extLink(githubLinks.release) } >
+1
View File
@@ -15,6 +15,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
);
document.title = i18n.t("manifest.name");
analytics.page("collection_list");
function MainPage(): React.ReactElement
{
+2 -1
View File
@@ -88,7 +88,8 @@ export default defineConfig([
"@typescript-eslint/no-unused-vars": ["warn"],
"prefer-const": ["warn"],
"@stylistic/padded-blocks": ["warn"],
"no-empty": ["off"]
"no-empty": ["off"],
"@stylistic/eol-last": ["warn"]
}
},
{
+2
View File
@@ -0,0 +1,2 @@
export { default as userPropertiesStorage } from "./utils/userPropertiesStorage";
export { default as trackError } from "./utils/trackError";
+8
View File
@@ -0,0 +1,8 @@
export default function trackError(name: string, error: Error): void
{
analytics.track(name, {
name: error.name,
message: error.message,
stack: error.stack ?? "no_stack"
});
}
@@ -0,0 +1,35 @@
import { cloudDisabled, collectionCount } from "@/features/collectionStorage";
import { settings } from "@/utils/settings";
import { WxtStorageItem } from "wxt/storage";
// @ts-expect-error we don't need to implement a full storage item
const userPropertiesStorage: WxtStorageItem<Record<string, string>, any> =
{
getValue: async (): Promise<UserProperties> =>
{
console.log("userPropertiesStorage.getValue");
const properties: UserProperties =
{
cloud_used: await cloudDisabled.getValue() ? "-1" : (await browser.storage.sync.getBytesInUse() / 102400).toString(),
collection_count: (await collectionCount.getValue()).toString()
};
for (const key of Object.keys(settings))
{
const value = await settings[key as keyof typeof settings].getValue();
properties[`option_${key}`] = value.valueOf().toString();
}
return properties;
},
setValue: async () => { }
};
export default userPropertiesStorage;
export type UserProperties =
{
collection_count: string;
cloud_used: string;
[key: `option_${string}`]: string;
};
@@ -1,3 +1,4 @@
import { trackError } from "@/features/analytics";
import { CollectionItem } from "@/models/CollectionModels";
import getLogger from "@/utils/getLogger";
import { collectionStorage } from "./collectionStorage";
@@ -32,6 +33,7 @@ export default async function getCollections(): Promise<[CollectionItem[], Cloud
{
logger("Failed to get cloud storage");
console.error(ex);
trackError("cloud_get_error", ex as Error);
return [await getCollectionsFromLocal(), "parse_error"];
}
}
@@ -1,3 +1,4 @@
import { trackError } from "@/features/analytics";
import { CollectionItem } from "@/models/CollectionModels";
import getLogger from "@/utils/getLogger";
import { collectionStorage } from "./collectionStorage";
@@ -37,5 +38,6 @@ async function replaceLocalWithCloud(): Promise<void>
{
logger("Failed to get cloud storage");
console.error(ex);
trackError("conflict_resolve_with_cloud_error", ex as Error);
}
}
@@ -1,3 +1,4 @@
import { trackError } from "@/features/analytics";
import { CollectionItem, GraphicsStorage } from "@/models/CollectionModels";
import getLogger from "@/utils/getLogger";
import sendNotification from "@/utils/sendNotification";
@@ -26,6 +27,7 @@ export default async function saveCollections(
{
logger("Failed to save cloud storage");
console.error(ex);
trackError("cloud_save_error", ex as Error);
if ((ex as Error).message.includes("MAX_WRITE_OPERATIONS_PER_MINUTE"))
await sendNotification({
@@ -39,7 +39,10 @@ export default function WelcomeDialog(): React.ReactElement
<fui.DialogActions>
<fui.DialogTrigger disableButtonEnhancement>
<fui.Button appearance="primary" as="a" { ...extLink(v3blogPost) }>
<fui.Button
appearance="primary" as="a" { ...extLink(v3blogPost) }
onClick={ () => analytics.track("visit_blog_button_click") }
>
{ i18n.t("features.v3welcome.actions.visit_blog") }
</fui.Button>
</fui.DialogTrigger>
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "tabs-aside",
"private": true,
"version": "3.0.0-rc1",
"version": "3.0.0-rc2",
"type": "module",
"scripts": {
"dev": "wxt",
@@ -20,6 +20,7 @@
"@fluentui/react-components": "^9.63.0",
"@fluentui/react-icons": "^2.0.298",
"@webext-core/messaging": "^2.2.0",
"@wxt-dev/analytics": "^0.4.1",
"@wxt-dev/i18n": "^0.2.3",
"lzutf8": "^0.6.3",
"react": "^18.3.1",
+2
View File
@@ -1,3 +1,4 @@
import { trackError } from "@/features/analytics";
import { GraphicsStorage } from "@/models/CollectionModels";
import { defineExtensionMessaging, ExtensionMessenger } from "@webext-core/messaging";
@@ -21,6 +22,7 @@ export const sendMessage: ExtensionMessenger<ProtocolMap>["sendMessage"] = async
catch (ex)
{
console.error(ex);
trackError("messaging_error", ex as Error);
return undefined!;
}
};
+2
View File
@@ -24,6 +24,8 @@ export default async function saveTabsToCollection(closeTabs: boolean): Promise<
if (closeTabs)
await browser.tabs.remove(tabsToClose.map(i => i.id!));
analytics.track(closeTabs ? "set_aside" : "save");
return collection;
}
+3 -1
View File
@@ -1,3 +1,4 @@
import { trackError } from "@/features/analytics";
import { PublicPath } from "wxt/browser";
export default async function sendNotification(props: NotificationProps): Promise<void>
@@ -13,8 +14,9 @@ export default async function sendNotification(props: NotificationProps): Promis
}
catch (ex)
{
console.error("Error while showing cloud error notification (probably because of user restrictions)");
console.error("Error while showing notification (probably because of user restrictions)");
console.error(ex);
trackError("notification_error", ex as Error);
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { ConfigEnv, defineConfig, UserManifest } from "wxt";
// See https://wxt.dev/api/config.html
export default defineConfig({
modules: ["@wxt-dev/module-react", "@wxt-dev/i18n/module"],
modules: ["@wxt-dev/module-react", "@wxt-dev/i18n/module", "@wxt-dev/analytics/module"],
imports: {
dirsScanOptions:
{
+12
View File
@@ -2031,6 +2031,13 @@
serialize-error "^11.0.0"
webextension-polyfill "^0.10.0"
"@wxt-dev/analytics@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@wxt-dev/analytics/-/analytics-0.4.1.tgz#23ecbff34eec04690e64f8f9850832baa207486c"
integrity sha512-BDRyfIxO7MKoXLM2jCxX+EVCDjB5jjWGM2GWlU0mYQwi+IzSwMUHnw0UMnDeQ1Zr6yiyjopgCdg4XGaV9QsLJg==
dependencies:
ua-parser-js "^1.0.38"
"@wxt-dev/i18n@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@wxt-dev/i18n/-/i18n-0.2.3.tgz#5688cbdf5324e86fbd65d585073121bf8d493085"
@@ -6398,6 +6405,11 @@ typescript@^5.8.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
ua-parser-js@^1.0.38:
version "1.0.40"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.40.tgz#ac6aff4fd8ea3e794a6aa743ec9c2fc29e75b675"
integrity sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==
ufo@^1.5.4:
version "1.6.1"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b"