mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add shared analytics types, events, consent logic, and i18n strings
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ConsentState } from "./types.js";
|
||||||
|
|
||||||
|
export function shouldShowConsent(consent: ConsentState, serverEnabled: boolean): boolean {
|
||||||
|
if (!serverEnabled) return false;
|
||||||
|
if (consent.analyticsEnabled !== null) return false;
|
||||||
|
if (consent.analyticsConsentShownAt === null) return true;
|
||||||
|
if (consent.analyticsConsentRemindAt === null) return false;
|
||||||
|
return Date.now() >= consent.analyticsConsentRemindAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isConsentEnabled(consent: ConsentState, serverEnabled: boolean): boolean {
|
||||||
|
if (!serverEnabled) return false;
|
||||||
|
return consent.analyticsEnabled === true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
export const ANALYTICS_EVENTS = {
|
||||||
|
TOOL_USED: "tool_used",
|
||||||
|
SEARCH: "search",
|
||||||
|
PIPELINE_EXECUTED: "pipeline_executed",
|
||||||
|
AI_BUNDLE_ACTION: "ai_bundle_action",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type AnalyticsEvent = (typeof ANALYTICS_EVENTS)[keyof typeof ANALYTICS_EVENTS];
|
||||||
|
|
||||||
|
export interface ToolUsedProperties {
|
||||||
|
tool_id: string;
|
||||||
|
status: "completed" | "failed";
|
||||||
|
duration_ms: number;
|
||||||
|
category: string;
|
||||||
|
is_ai_tool: boolean;
|
||||||
|
params?: Record<string, string | number | boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchProperties {
|
||||||
|
query: string;
|
||||||
|
results_count: number;
|
||||||
|
clicked_tool_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PipelineExecutedProperties {
|
||||||
|
step_count: number;
|
||||||
|
tool_ids: string[];
|
||||||
|
is_batch: boolean;
|
||||||
|
file_count?: number;
|
||||||
|
duration_ms: number;
|
||||||
|
status: "completed" | "failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiBundleActionProperties {
|
||||||
|
bundle_id: string;
|
||||||
|
action: "installed" | "uninstalled";
|
||||||
|
duration_ms: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export interface AnalyticsConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
posthogApiKey: string;
|
||||||
|
posthogHost: string;
|
||||||
|
sentryDsn: string;
|
||||||
|
sampleRate: number;
|
||||||
|
instanceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConsentState {
|
||||||
|
analyticsEnabled: boolean | null;
|
||||||
|
analyticsConsentShownAt: number | null;
|
||||||
|
analyticsConsentRemindAt: number | null;
|
||||||
|
}
|
||||||
@@ -301,6 +301,33 @@ export const en = {
|
|||||||
help: "Help",
|
help: "Help",
|
||||||
settings: "Settings",
|
settings: "Settings",
|
||||||
},
|
},
|
||||||
|
analytics: {
|
||||||
|
consentTitle: "Make ashim better for you",
|
||||||
|
consentDescription:
|
||||||
|
'Anonymous product analytics — like "crop tool used" or "an error occurred" — help us fix bugs faster and build the tools you actually want.',
|
||||||
|
consentPrivacy: "Your images never leave your machine. Ever.",
|
||||||
|
whatShared: "What's shared:",
|
||||||
|
whatSharedItems: [
|
||||||
|
"Which tools you use",
|
||||||
|
"Error reports (no file data)",
|
||||||
|
"App version and performance",
|
||||||
|
],
|
||||||
|
whatNever: "What NEVER leaves your machine:",
|
||||||
|
whatNeverItems: [
|
||||||
|
"Your images, PDFs, and files",
|
||||||
|
"File names and contents",
|
||||||
|
"Any personal information",
|
||||||
|
],
|
||||||
|
consentProviders: "Data is sent to PostHog (analytics) and Sentry (errors) — both open-source.",
|
||||||
|
consentChangeable: "You can change this anytime in Settings.",
|
||||||
|
acceptButton: "Sure, sounds good",
|
||||||
|
declineButton: "Maybe later",
|
||||||
|
settingsTitle: "Product Analytics",
|
||||||
|
settingsDescription: "Share anonymous usage data to help improve ashim.",
|
||||||
|
settingsPrivacy: "Your images never leave your machine.",
|
||||||
|
settingsDisabledByAdmin: "Product analytics has been disabled by the server administrator.",
|
||||||
|
learnMore: "Learn more",
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type TranslationKeys = typeof en;
|
export type TranslationKeys = typeof en;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
export * from "./analytics/consent.js";
|
||||||
|
export * from "./analytics/events.js";
|
||||||
|
export * from "./analytics/types.js";
|
||||||
export * from "./constants.js";
|
export * from "./constants.js";
|
||||||
export * from "./features.js";
|
export * from "./features.js";
|
||||||
export * from "./i18n/index.js";
|
export * from "./i18n/index.js";
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { isConsentEnabled, shouldShowConsent } from "@ashim/shared";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("shouldShowConsent", () => {
|
||||||
|
it("returns false when server has analytics disabled", () => {
|
||||||
|
expect(
|
||||||
|
shouldShowConsent(
|
||||||
|
{
|
||||||
|
analyticsEnabled: null,
|
||||||
|
analyticsConsentShownAt: null,
|
||||||
|
analyticsConsentRemindAt: null,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true for a fresh user who has never been asked", () => {
|
||||||
|
expect(
|
||||||
|
shouldShowConsent(
|
||||||
|
{
|
||||||
|
analyticsEnabled: null,
|
||||||
|
analyticsConsentShownAt: null,
|
||||||
|
analyticsConsentRemindAt: null,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when user already opted in", () => {
|
||||||
|
expect(
|
||||||
|
shouldShowConsent(
|
||||||
|
{
|
||||||
|
analyticsEnabled: true,
|
||||||
|
analyticsConsentShownAt: Date.now(),
|
||||||
|
analyticsConsentRemindAt: null,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when user explicitly declined", () => {
|
||||||
|
expect(
|
||||||
|
shouldShowConsent(
|
||||||
|
{
|
||||||
|
analyticsEnabled: false,
|
||||||
|
analyticsConsentShownAt: Date.now(),
|
||||||
|
analyticsConsentRemindAt: null,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when remind-at is in the future", () => {
|
||||||
|
expect(
|
||||||
|
shouldShowConsent(
|
||||||
|
{
|
||||||
|
analyticsEnabled: null,
|
||||||
|
analyticsConsentShownAt: Date.now() - 86400000,
|
||||||
|
analyticsConsentRemindAt: Date.now() + 86400000,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when remind-at has passed", () => {
|
||||||
|
expect(
|
||||||
|
shouldShowConsent(
|
||||||
|
{
|
||||||
|
analyticsEnabled: null,
|
||||||
|
analyticsConsentShownAt: Date.now() - 86400000 * 8,
|
||||||
|
analyticsConsentRemindAt: Date.now() - 86400000,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isConsentEnabled", () => {
|
||||||
|
it("returns false when server disabled", () => {
|
||||||
|
expect(
|
||||||
|
isConsentEnabled(
|
||||||
|
{
|
||||||
|
analyticsEnabled: true,
|
||||||
|
analyticsConsentShownAt: Date.now(),
|
||||||
|
analyticsConsentRemindAt: null,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when user has not consented", () => {
|
||||||
|
expect(
|
||||||
|
isConsentEnabled(
|
||||||
|
{
|
||||||
|
analyticsEnabled: null,
|
||||||
|
analyticsConsentShownAt: null,
|
||||||
|
analyticsConsentRemindAt: null,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when user opted in and server enabled", () => {
|
||||||
|
expect(
|
||||||
|
isConsentEnabled(
|
||||||
|
{
|
||||||
|
analyticsEnabled: true,
|
||||||
|
analyticsConsentShownAt: Date.now(),
|
||||||
|
analyticsConsentRemindAt: null,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user