feat(frontend): integrate with Reo.dev (#11251)

This commit is contained in:
Hiep Le 2025-10-06 21:37:25 +07:00 committed by GitHub
parent fbf0429434
commit c62a6616db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 265 additions and 0 deletions

14
docs/reo-init.js Normal file
View File

@ -0,0 +1,14 @@
// Reo.dev tracking initialization
(function() {
var e, t, n;
e = "6bac7145b4ee6ec";
t = function() {
Reo.init({clientID: "6bac7145b4ee6ec"});
};
n = document.createElement("script");
n.src = "https://static.reo.dev/" + e + "/reo.js";
n.defer = true;
n.onload = t;
document.head.appendChild(n);
})();

14
frontend/global.d.ts vendored
View File

@ -1,4 +1,18 @@
interface Window {
__APP_MODE__?: "saas" | "oss";
__GITHUB_CLIENT_ID__?: string | null;
Reo?: {
init: (config: { clientID: string }) => void;
identify: (identity: {
username: string;
type: "github" |"email";
other_identities?: Array<{
username: string;
type: "github" | "email";
}>;
firstname?: string;
lastname?: string;
company?: string;
}) => void;
};
}

View File

@ -0,0 +1,129 @@
import React from "react";
import { useConfig } from "./query/use-config";
import { useGitUser } from "./query/use-git-user";
import { getLoginMethod, LoginMethod } from "#/utils/local-storage";
import reoService, { ReoIdentity } from "#/utils/reo";
/**
* Maps login method to Reo identity type
*/
const mapLoginMethodToReoType = (method: LoginMethod): ReoIdentity["type"] => {
// Reo is not supporting gitlab and bitbucket.
switch (method) {
case LoginMethod.GITHUB:
return "github";
case LoginMethod.ENTERPRISE_SSO:
return "email";
default:
return "email";
}
};
/**
* Creates email identity object if email is available
*/
const buildEmailIdentity = (
email?: string | null,
): ReoIdentity["other_identities"] => {
if (!email) {
return undefined;
}
return [
{
username: email,
type: "email",
},
];
};
/**
* Parses full name into firstname and lastname
* Handles cases where name might be empty or only have one part
*/
const parseNameFields = (
fullName?: string | null,
): { firstname?: string; lastname?: string } => {
if (!fullName) {
return {};
}
const [firstname, ...rest] = fullName.split(" ");
if (!firstname) {
return {};
}
return {
firstname,
lastname: rest.length > 0 ? rest.join(" ") : undefined,
};
};
/**
* Builds complete Reo identity from user data and login method
*/
const buildReoIdentity = (
user: {
login: string;
email?: string | null;
name?: string | null;
company?: string | null;
},
loginMethod: LoginMethod,
): ReoIdentity => {
const { firstname, lastname } = parseNameFields(user.name);
return {
username: user.login,
type: mapLoginMethodToReoType(loginMethod),
other_identities: buildEmailIdentity(user.email),
firstname,
lastname,
company: user.company || undefined,
};
};
/**
* Hook to handle Reo.dev tracking integration
* Only active in SaaS mode
*/
export const useReoTracking = () => {
const { data: config } = useConfig();
const { data: user } = useGitUser();
const [hasIdentified, setHasIdentified] = React.useState(false);
// Initialize Reo.dev when in SaaS mode
React.useEffect(() => {
const initReo = async () => {
if (config?.APP_MODE === "saas" && !reoService.isInitialized()) {
await reoService.init();
}
};
initReo();
}, [config?.APP_MODE]);
// Identify user when user data is available and we're in SaaS mode
React.useEffect(() => {
if (
config?.APP_MODE !== "saas" ||
!user ||
hasIdentified ||
!reoService.isInitialized()
) {
return;
}
const loginMethod = getLoginMethod();
if (!loginMethod) {
return;
}
// Build identity payload from user data
const identity = buildReoIdentity(user, loginMethod);
// Identify user in Reo
reoService.identify(identity);
setHasIdentified(true);
}, [config?.APP_MODE, user, hasIdentified]);
};

View File

@ -24,6 +24,7 @@ import { displaySuccessToast } from "#/utils/custom-toast-handlers";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
import { useAutoLogin } from "#/hooks/use-auto-login";
import { useAuthCallback } from "#/hooks/use-auth-callback";
import { useReoTracking } from "#/hooks/use-reo-tracking";
import { LOCAL_STORAGE_KEYS } from "#/utils/local-storage";
import { EmailVerificationGuard } from "#/components/features/guards/email-verification-guard";
import { MaintenanceBanner } from "#/components/features/maintenance/maintenance-banner";
@ -96,6 +97,9 @@ export default function MainApp() {
// Handle authentication callback and set login method after successful authentication
useAuthCallback();
// Initialize Reo.dev tracking in SaaS mode
useReoTracking();
React.useEffect(() => {
// Don't change language when on TOS page
if (!isOnTosPage && settings?.LANGUAGE) {

104
frontend/src/utils/reo.ts Normal file
View File

@ -0,0 +1,104 @@
/**
* Reo.dev tracking service for SaaS mode
* Tracks developer activity and engagement in the product
* Using CDN approach for better TypeScript compatibility
*/
export interface ReoIdentity {
username: string;
type: "github" | "email";
other_identities?: Array<{
username: string;
type: "github" | "email";
}>;
firstname?: string;
lastname?: string;
company?: string;
}
const REO_CLIENT_ID = "6bac7145b4ee6ec";
class ReoService {
private initialized = false;
private scriptLoaded = false;
/**
* Load and initialize the Reo.dev tracking script from CDN
*/
async init(): Promise<void> {
if (this.initialized) {
return;
}
try {
// Load the Reo script dynamically from CDN
await this.loadScript();
// Initialize Reo with client ID
if (window.Reo) {
window.Reo.init({ clientID: REO_CLIENT_ID });
this.initialized = true;
}
} catch (error) {
console.error("Failed to initialize Reo.dev tracking:", error);
}
}
/**
* Load the Reo.dev script from CDN
*/
private loadScript(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.scriptLoaded) {
resolve();
return;
}
const script = document.createElement("script");
script.src = `https://static.reo.dev/${REO_CLIENT_ID}/reo.js`;
script.defer = true;
script.onload = () => {
this.scriptLoaded = true;
resolve();
};
script.onerror = () => {
reject(new Error("Failed to load Reo.dev script"));
};
document.head.appendChild(script);
});
}
/**
* Identify a user in Reo.dev tracking
* Should be called after successful login
*/
identify(identity: ReoIdentity): void {
if (!this.initialized) {
console.warn("Reo.dev not initialized. Call init() first.");
return;
}
try {
if (window.Reo) {
window.Reo.identify(identity);
}
} catch (error) {
console.error("Failed to identify user in Reo.dev:", error);
}
}
/**
* Check if Reo.dev is initialized
*/
isInitialized(): boolean {
return this.initialized;
}
}
const reoService = new ReoService();
export default reoService;