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:
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -26,3 +26,5 @@ web-ext.config.ts
|
||||
*.sw?
|
||||
|
||||
web-ext.config.js
|
||||
|
||||
.env*
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) } >
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as userPropertiesStorage } from "./utils/userPropertiesStorage";
|
||||
export { default as trackError } from "./utils/trackError";
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user