Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b0caa07
feat: add app site type support to database schema and API
lukyrys Feb 26, 2026
9eb5eb0
feat: add app_version and device_model tracking for SDK clients
lukyrys Feb 26, 2026
5c81b20
feat: RFC 7231 User-Agent parsing for SDK clients
lukyrys Feb 26, 2026
5b8249b
feat: platform-aware filter system (web vs app)
lukyrys Feb 26, 2026
ed6cfc3
feat: AddSite dialog with web/app platform selector
lukyrys Feb 26, 2026
07c1cf5
feat: adapt dashboard UI for app site types
lukyrys Feb 26, 2026
c774bbd
feat: expose app_version and device_model in analytics endpoints
lukyrys Feb 26, 2026
1767d6c
feat: adapt events page and event log for app sites
lukyrys Feb 26, 2026
4ac1a85
feat: adapt sessions page and session details for app sites
lukyrys Feb 26, 2026
d7c4f0e
feat: adapt users pages and user sidebar for app sites
lukyrys Feb 26, 2026
e920af7
feat: wire app-aware filters to funnels, goals, and journeys pages
lukyrys Feb 26, 2026
4cd17d9
feat: SDK integration instructions and error tracking for app sites
lukyrys Feb 26, 2026
cab37a0
fix: translations for all 11 languages
lukyrys Feb 26, 2026
94b7f0e
fix: hide block bot traffic toggle for app sites
lukyrys Feb 26, 2026
62bfbf3
feat: add app icon upload with bytea storage and canvas resize
lukyrys Feb 26, 2026
ac84e2f
fix: update SDK link to renamed rybbit-flutter-sdk repo
lukyrys Feb 26, 2026
dbbdd4a
fix: render app screen path as plain text instead of web link
lukyrys Mar 10, 2026
f607f47
fix: remove redundant v prefix from app_version display
lukyrys Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 137 additions & 84 deletions client/messages/cs.json

Large diffs are not rendered by default.

299 changes: 174 additions & 125 deletions client/messages/de.json

Large diffs are not rendered by default.

299 changes: 174 additions & 125 deletions client/messages/en.json

Large diffs are not rendered by default.

299 changes: 174 additions & 125 deletions client/messages/es.json

Large diffs are not rendered by default.

299 changes: 174 additions & 125 deletions client/messages/fr.json

Large diffs are not rendered by default.

299 changes: 174 additions & 125 deletions client/messages/it.json

Large diffs are not rendered by default.

299 changes: 174 additions & 125 deletions client/messages/ja.json

Large diffs are not rendered by default.

299 changes: 174 additions & 125 deletions client/messages/ko.json

Large diffs are not rendered by default.

299 changes: 174 additions & 125 deletions client/messages/pl.json

Large diffs are not rendered by default.

299 changes: 174 additions & 125 deletions client/messages/pt.json

Large diffs are not rendered by default.

299 changes: 174 additions & 125 deletions client/messages/zh.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions client/src/api/admin/endpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
export {
addSite,
deleteSite,
deleteSiteIcon,
updateSiteConfig,
fetchSite,
fetchSitesFromOrg,
fetchSiteHasData,
fetchSiteIsPublic,
uploadSiteIcon,
verifyScript,
} from "./sites";
export type { SiteResponse, GetSitesFromOrgResponse, VerifyScriptResponse } from "./sites";
Expand Down
18 changes: 18 additions & 0 deletions client/src/api/admin/endpoints/sites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type SiteResponse = {
siteId: number;
name: string;
domain: string;
type?: "web" | "app";
createdAt: string;
updatedAt: string;
createdBy: string;
Expand Down Expand Up @@ -45,6 +46,7 @@ export type GetSitesFromOrgResponse = {
siteId: number;
name: string;
domain: string;
type?: "web" | "app";
createdAt: string;
updatedAt: string;
createdBy: string;
Expand Down Expand Up @@ -78,6 +80,7 @@ export function addSite(
isPublic?: boolean;
saltUserIds?: boolean;
blockBots?: boolean;
type?: "web" | "app";
}
) {
return authedFetch<{ siteId: number }>(`/organizations/${organizationId}/sites`, undefined, {
Expand All @@ -88,6 +91,7 @@ export function addSite(
public: settings?.isPublic || false,
saltUserIds: settings?.saltUserIds || false,
blockBots: settings?.blockBots === undefined ? true : settings?.blockBots,
type: settings?.type || "web",
},
headers: {
"Content-Type": "application/json",
Expand Down Expand Up @@ -156,3 +160,17 @@ export interface VerifyScriptResponse {
export function verifyScript(siteId: number | string) {
return authedFetch<VerifyScriptResponse>(`/sites/${siteId}/verify-script`);
}

export function uploadSiteIcon(siteId: number, icon: string) {
return authedFetch<{ success: boolean }>(`/sites/${siteId}/icon`, undefined, {
method: "PUT",
data: { icon },
headers: { "Content-Type": "application/json" },
});
}

export function deleteSiteIcon(siteId: number) {
return authedFetch<{ success: boolean }>(`/sites/${siteId}/icon`, undefined, {
method: "DELETE",
});
}
2 changes: 2 additions & 0 deletions client/src/api/analytics/endpoints/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export type Event = {
device_type: string;
type: string;
traits?: Record<string, unknown> | null;
device_model: string;
app_version: string;
};

// Response types for cursor-based API
Expand Down
4 changes: 4 additions & 0 deletions client/src/api/analytics/endpoints/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type GetSessionsResponse = {
operating_system_version: string;
screen_width: number;
screen_height: number;
device_model: string;
app_version: string;
referrer: string;
channel: string;
hostname: string;
Expand Down Expand Up @@ -56,6 +58,8 @@ export interface SessionDetails {
city: string;
language: string;
device_type: string;
device_model: string;
app_version: string;
browser: string;
browser_version: string;
operating_system: string;
Expand Down
4 changes: 4 additions & 0 deletions client/src/api/analytics/endpoints/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export type UsersResponse = {
browser: string;
operating_system: string;
device_type: string;
device_model: string;
app_version: string;
referrer: string;
channel: string;
pageviews: number;
Expand All @@ -40,6 +42,8 @@ export type UserInfo = {
city: string;
language: string;
device_type: string;
device_model: string;
app_version: string;
browser: string;
browser_version: string;
operating_system: string;
Expand Down
82 changes: 59 additions & 23 deletions client/src/app/[site]/components/Header/NoData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SiContentful,
SiDocusaurus,
SiDrupal,
SiFlutter,
SiFramer,
SiGatsby,
SiGhost,
Expand Down Expand Up @@ -74,6 +75,9 @@ export function NoData() {
const { data: siteHasData, isLoading } = useSiteHasData(site);
const { data: siteMetadata, isLoading: isLoadingSiteMetadata } = useGetSite(site);

const siteType = siteMetadata?.type ?? "web";
const isApp = siteType === "app";

if (!siteHasData && !isLoading && !isLoadingSiteMetadata) {
return (
<>
Expand All @@ -84,30 +88,60 @@ export function NoData() {
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500"></span>
</span>
<div className="font-medium">{t("Waiting for analytics from {name}...", { name: siteMetadata?.name ?? "" })}</div>
<div className="font-medium">{isApp ? t("Waiting for analytics from your app...") : t("Waiting for analytics from {name}...", { name: siteMetadata?.name ?? "" })}</div>
</div>
<div className="text-xs text-muted-foreground">{t("Place this snippet in the {headTag} of your website:", { headTag: "<head>" })}</div>
<CodeSnippet
language="HTML"
code={`<script\n src="${globalThis.location.origin}/api/script.js"\n data-site-id="${siteMetadata?.id ?? siteMetadata?.siteId}"\n defer\n></script>`}
className="text-xs"
/>
<span className="text-xs text-muted-foreground">
{t("See our")}{" "}
<ExternalLink href="https://rybbit.com/docs/script">
{t("docs")}
</ExternalLink>{" "}
{t("for more information, or")}{" "}
<ExternalLink href="https://rybbit.com/docs/script-troubleshooting">
{t("troubleshoot")}
</ExternalLink>{" "}
{t("if your script isn't sending traffic.")}
</span>
{/* {siteMetadata?.siteId && <VerifyInstallation siteId={siteMetadata.siteId} />} */}
{/* Framework Guide Cards */}
<div className="">
<h2 className="text-sm font-medium mb-4">{t("Platform Guides")}</h2>
<div className="flex flex-wrap gap-2">
{isApp ? (
<>
<div className="text-xs text-muted-foreground">{t("Add the Rybbit SDK to your app:")}</div>
<CodeSnippet
language="dart"
code={`await Rybbit.init(\n host: '${globalThis.location?.origin ?? "https://your-rybbit-instance.com"}',\n siteId: '${siteMetadata?.id ?? siteMetadata?.siteId}',\n);`}
className="text-xs"
/>
<span className="text-xs text-muted-foreground">
{t("See the")}{" "}
<ExternalLink href="https://github.com/nks-hub/rybbit-flutter-sdk">
{t("Flutter SDK documentation")}
</ExternalLink>{" "}
{t("for installation and usage instructions.")}
</span>
{/* SDK Guide Cards */}
<div className="">
<h2 className="text-sm font-medium mb-4">{t("Platform Guides")}</h2>
<div className="flex flex-wrap gap-2">
<Card
icon={<SiFlutter className="w-5 h-5" />}
title="Flutter"
description=""
href="https://github.com/nks-hub/rybbit-flutter-sdk"
/>
</div>
</div>
</>
) : (
<>
<div className="text-xs text-muted-foreground">{t("Place this snippet in the {headTag} of your website:", { headTag: "<head>" })}</div>
<CodeSnippet
language="HTML"
code={`<script\n src="${globalThis.location?.origin ?? "https://your-rybbit-instance.com"}/api/script.js"\n data-site-id="${siteMetadata?.id ?? siteMetadata?.siteId}"\n defer\n></script>`}
className="text-xs"
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<span className="text-xs text-muted-foreground">
{t("See our")}{" "}
<ExternalLink href="https://rybbit.com/docs/script">
{t("docs")}
</ExternalLink>{" "}
{t("for more information, or")}{" "}
<ExternalLink href="https://rybbit.com/docs/script-troubleshooting">
{t("troubleshoot")}
</ExternalLink>{" "}
{t("if your script isn't sending traffic.")}
</span>
{/* {siteMetadata?.siteId && <VerifyInstallation siteId={siteMetadata.siteId} />} */}
{/* Framework Guide Cards */}
<div className="">
<h2 className="text-sm font-medium mb-4">{t("Platform Guides")}</h2>
<div className="flex flex-wrap gap-2">
<Card
icon={<SiGoogletagmanager className="w-5 h-5" />}
title="Google Tag Manager"
Expand Down Expand Up @@ -321,6 +355,8 @@ export function NoData() {
/>
</div>
</div>
</>
)}
</div>
</Alert>
</>
Expand Down
29 changes: 16 additions & 13 deletions client/src/app/[site]/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function SidebarContent() {
const embed = useEmbedablePage();

const { data: site } = useGetSite(Number(pathname.split("/")[1]));
const isApp = site?.type === "app";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Prevent app/web nav flicker before site type resolves.

At Line 39, isApp is false until useGetSite returns, so app sites can briefly render web-only labels/tabs (e.g., “Web Analytics”, “Performance”, “Replay”) and then switch.

💡 Suggested patch
-  const { data: site } = useGetSite(Number(pathname.split("/")[1]));
+  const { data: site, isLoading: isSiteLoading } = useGetSite(Number(pathname.split("/")[1]));
   const isApp = site?.type === "app";
@@
-        <SidebarComponents.SectionHeader>{isApp ? t("Analytics") : t("Web Analytics")}</SidebarComponents.SectionHeader>
+        <SidebarComponents.SectionHeader>
+          {isSiteLoading ? t("Analytics") : isApp ? t("Analytics") : t("Web Analytics")}
+        </SidebarComponents.SectionHeader>
@@
-        {IS_CLOUD && !isApp && (
+        {IS_CLOUD && !isSiteLoading && !isApp && (
@@
-        {!isApp && (
+        {!isSiteLoading && !isApp && (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/app/`[site]/components/Sidebar/Sidebar.tsx at line 39, isApp is
computed as false while useGetSite is still loading, causing web-only tabs to
flash before site type resolves; change the isApp computation to be tri-state
(true/false/undefined) by using the site value directly (e.g., const isApp =
site ? site.type === "app" : undefined) and update all render conditions in
Sidebar.tsx to check explicitly for isApp === true or isApp === false so that
when isApp is undefined the component avoids rendering app-only or web-only
labels/tabs until the site data has resolved (useGetSite).


// Check which tab is active based on the current path
const getTabPath = (tabName: string) => {
Expand Down Expand Up @@ -70,7 +71,7 @@ function SidebarContent() {
<SiteSelector />
</div>
<div className="flex flex-col p-3 pt-1">
<SidebarComponents.SectionHeader>{t("Web Analytics")}</SidebarComponents.SectionHeader>
<SidebarComponents.SectionHeader>{isApp ? t("Analytics") : t("Web Analytics")}</SidebarComponents.SectionHeader>
<SidebarComponents.Item
label={t("Main")}
active={isActiveTab("main")}
Expand All @@ -85,13 +86,13 @@ function SidebarContent() {
/>
{IS_CLOUD && (
<SidebarComponents.Item
label={t("Pages")}
label={isApp ? t("Screens") : t("Pages")}
active={isActiveTab("pages")}
href={getTabPath("pages")}
icon={<File className="w-4 h-4" />}
/>
)}
{IS_CLOUD && (
{IS_CLOUD && !isApp && (
<SidebarComponents.Item
label={t("Performance")}
active={isActiveTab("performance")}
Expand All @@ -114,16 +115,18 @@ function SidebarContent() {
/>
</div>
<SidebarComponents.SectionHeader>{t("Product Analytics")}</SidebarComponents.SectionHeader>
<div className="hidden md:block">
{!subscription?.planName?.startsWith("appsumo") && !isSubscriptionLoading && (
<SidebarComponents.Item
label={t("Replay")}
active={isActiveTab("replay")}
href={getTabPath("replay")}
icon={<Video className="w-4 h-4" />}
/>
)}
</div>
{!isApp && (
<div className="hidden md:block">
{!subscription?.planName?.startsWith("appsumo") && !isSubscriptionLoading && (
<SidebarComponents.Item
label={t("Replay")}
active={isActiveTab("replay")}
href={getTabPath("replay")}
icon={<Video className="w-4 h-4" />}
/>
)}
</div>
)}
<SidebarComponents.Item
label={t("Funnels")}
active={isActiveTab("funnels")}
Expand Down
9 changes: 6 additions & 3 deletions client/src/app/[site]/components/Sidebar/SiteSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChevronDown, Plus } from "lucide-react";
import { ChevronDown, Plus, Smartphone } from "lucide-react";
import { useExtracted } from "next-intl";
import { usePathname, useRouter } from "next/navigation";
import { useState, Suspense } from "react";
Expand Down Expand Up @@ -104,8 +104,11 @@ function SiteSelectorContent({ onSiteSelect }: { onSiteSelect: () => void }) {
)}
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<Favicon domain={site.domain} className="w-4 h-4 shrink-0" />
<Favicon domain={site.domain} className="w-4 h-4 shrink-0" siteType={site.type} siteId={site.siteId} />
<div className="text-sm text-neutral-900 dark:text-white truncate">{site.name}</div>
{site.type && site.type !== "web" && (
<Smartphone className="h-3 w-3 text-neutral-400 shrink-0" />
)}
</div>
<div className="flex items-center gap-3">
<div className="text-xs text-neutral-600 dark:text-neutral-300 whitespace-nowrap">
Expand Down Expand Up @@ -155,7 +158,7 @@ function SiteSelectorWrapper() {
<PopoverTrigger asChild>
{site ? (
<button className="flex gap-2 items-center border border-neutral-200 dark:border-neutral-800 rounded-lg py-1.5 px-3 justify-start cursor-pointer hover:bg-neutral-150 dark:hover:bg-neutral-800/50 transition-colors h-[36px] w-full">
<Favicon domain={site.domain} className="w-5 h-5" />
<Favicon domain={site.domain} className="w-5 h-5" siteType={site.type} siteId={site.siteId} />
<div className="text-neutral-900 dark:text-white truncate text-sm flex-1 text-left">{site.name}</div>
{!embed && <ChevronDown className="w-4 h-4 text-neutral-600 dark:text-neutral-400" />}
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const PAGE_METRICS: { param: FilterParameter; filename: string }[] = [
const DEVICE_METRICS: { param: FilterParameter; filename: string }[] = [
{ param: "browser", filename: "browsers.csv" },
{ param: "device_type", filename: "devices.csv" },
{ param: "device_model", filename: "device-models.csv" },
{ param: "app_version", filename: "app-versions.csv" },
{ param: "operating_system", filename: "operating-systems.csv" },
{ param: "dimensions", filename: "screen-dimensions.csv" },
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export function FilterComponent({
case "region": return t("Region");
case "city": return t("City");
case "device_type": return t("Device Type");
case "device_model": return t("Device Model");
case "app_version": return t("App Version");
case "operating_system": return t("Operating System");
case "operating_system_version": return t("Operating System Version");
case "browser": return t("Browser");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export function Filters({ availableFilters }: { availableFilters?: FilterParamet
switch (parameter) {
case "country": return t("Country");
case "device_type": return t("Device Type");
case "device_model": return t("Device Model");
case "app_version": return t("App Version");
case "operating_system": return t("OS");
case "browser": return t("Browser");
case "referrer": return t("Referrer");
Expand Down
10 changes: 10 additions & 0 deletions client/src/app/[site]/components/SubHeader/Filters/const.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ export const FilterOptions: {
value: "device_type",
icon: <TabletSmartphone className="h-4 w-4" />,
},
{
label: "Device Model",
value: "device_model",
icon: <TabletSmartphone className="h-4 w-4" />,
},
{
label: "App Version",
value: "app_version",
icon: <Tag className="h-4 w-4" />,
},
{
label: "Operating System",
value: "operating_system",
Expand Down
4 changes: 4 additions & 0 deletions client/src/app/[site]/components/SubHeader/Filters/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export function getParameterNameLabel(parameter: FilterParameter) {
return "Country";
case "device_type":
return "Device Type";
case "device_model":
return "Device Model";
case "app_version":
return "App Version";
case "operating_system":
return "OS";
case "browser":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export function EnableErrorTracking() {
</AlertTitle>
<AlertDescription className="text-sm text-neutral-700/80 dark:text-neutral-300/80">
<div className="mb-2">
{t("Error tracking captures JavaScript errors and exceptions from your application.")} <b>{t("Note:")}</b> {t("Enabling error tracking will increase your event usage.")}
{siteMetadata?.type && siteMetadata.type !== "web"
? t("Error tracking captures errors and exceptions from your application.")
: t("Error tracking captures JavaScript errors and exceptions from your application.")} <b>{t("Note:")}</b> {t("Enabling error tracking will increase your event usage.")}
</div>
<Button
size="sm"
Expand Down
Loading
Loading