Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: react scan #445

Merged
merged 29 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9f82c27
feat: react scan and monitoring scripts
JBR90 Dec 11, 2024
c51c93d
text: playwright test for chat performance
JBR90 Dec 12, 2024
e39d0ef
Merge branch 'main' into feat-react-scan
JBR90 Dec 12, 2024
673ca87
chore: update lock file
JBR90 Dec 12, 2024
ad996b6
chore: remove unused import
JBR90 Dec 12, 2024
42a112e
fix: add missing env in test
JBR90 Dec 12, 2024
c942968
test: wait for window object
JBR90 Dec 12, 2024
0c2543b
chore: add logs for missing env
JBR90 Dec 12, 2024
def8117
chore: additional logs
JBR90 Dec 12, 2024
6028c38
chore: more logs
JBR90 Dec 12, 2024
733a131
chore: check window for env
JBR90 Dec 17, 2024
c862a12
chore: check for process and window
JBR90 Dec 17, 2024
e6add52
chore: remove window logs
JBR90 Dec 17, 2024
c4a5ac5
chore: try window object
JBR90 Dec 17, 2024
ebe31a9
fix: types
JBR90 Dec 17, 2024
3fb672f
chore: add csp wait
JBR90 Dec 17, 2024
51075df
chore: try without env
JBR90 Dec 17, 2024
2109477
chore : add env earlier
JBR90 Dec 17, 2024
2aadf15
chore: increase wait time
JBR90 Dec 17, 2024
91b018c
chore: remove state
JBR90 Dec 17, 2024
b648db7
chore: use react scan with out env
JBR90 Dec 17, 2024
76f3f81
chore: remove test
JBR90 Dec 18, 2024
e4cfa52
Merge branch 'main' into feat-react-scan
JBR90 Dec 18, 2024
c61de27
refactor: react scan hook
JBR90 Dec 18, 2024
2a4fd67
chore: add in disabled test
JBR90 Dec 18, 2024
b72488c
chore: change logger name
JBR90 Dec 18, 2024
87b04e0
chore: update package json
JBR90 Dec 18, 2024
94b2bad
chore: add comment to skipped test
JBR90 Dec 18, 2024
6161178
chore: alphabetical order
JBR90 Dec 18, 2024
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
1 change: 1 addition & 0 deletions apps/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"react-hot-toast": "^2.4.1",
"react-intersection-observer": "^9.6.0",
"react-markdown": "^9.0.0",
"react-scan": "^0.0.43",
"react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.5.3",
"remark-gfm": "^4.0.0",
Expand Down
5 changes: 5 additions & 0 deletions apps/nextjs/src/app/aila/page-contents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

import React from "react";

import { useReactScan } from "hooks/useReactScan";

import { Chat } from "@/components/AppComponents/Chat/Chat/chat";
import LessonPlanDisplay from "@/components/AppComponents/Chat/chat-lessonPlanDisplay";
import Layout from "@/components/AppComponents/Layout";
import { ChatProvider } from "@/components/ContextProviders/ChatProvider";
import LessonPlanTrackingProvider from "@/lib/analytics/lessonPlanTrackingContext";

const ChatPageContents = ({ id }: { readonly id: string }) => {
useReactScan({ component: LessonPlanDisplay, interval: 10000 });

return (
<Layout>
<LessonPlanTrackingProvider chatId={id}>
Expand Down
13 changes: 13 additions & 0 deletions apps/nextjs/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { Toaster } from "react-hot-toast";
import { Monitoring } from "react-scan/dist/core/monitor/params/next";

import { ClerkProvider } from "@clerk/nextjs";
import "@fontsource/lexend";
Expand Down Expand Up @@ -38,6 +39,11 @@ const provided_vercel_url =

const vercel_url = `https://${provided_vercel_url}`;

const reactScanApiKey = process.env.NEXT_PUBLIC_REACT_SCAN_KEY;
const addReactScanMonitor =
process.env.NEXT_PUBLIC_RENDER_MONITOR === "true" &&
reactScanApiKey !== undefined;

const lexend = Lexend({
subsets: ["latin"],
display: "swap",
Expand Down Expand Up @@ -74,6 +80,7 @@ export default async function RootLayout({
children,
}: Readonly<RootLayoutProps>) {
const nonce = headers().get("x-nonce");

if (!nonce) {
// Our middleware path matching excludes static paths like /_next/static/...
// If a static path becomes a 404, CSP headers aren't set
Expand All @@ -93,6 +100,12 @@ export default async function RootLayout({
GeistMono.variable,
)}
>
{addReactScanMonitor && (
<Monitoring
apiKey={reactScanApiKey}
url={process.env.NEXT_PUBLIC_REACT_SCAN_URL}
/>
)}
<Theme
accentColor="blue"
grayColor="olive"
Expand Down
149 changes: 149 additions & 0 deletions apps/nextjs/src/hooks/useReactScan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { useEffect } from "react";
import { getReport, scan } from "react-scan";

import { aiLogger } from "@oakai/logger";

const log = aiLogger("aila:chat-performance");

declare global {
interface Window {
NEXT_PUBLIC_ENABLE_RENDER_SCAN?: string;
}
}

function getWindowPropertyName<T>(component: React.ComponentType<T>): string {
return `reactScan${component.displayName ?? component.name ?? "UnknownComponent"}`;
}

function setWindowObjectForPlaywright<T>(
component: React.ComponentType<T> | undefined,
report: sortedReport,
) {
if (component) {
const windowPropertyName = getWindowPropertyName(component);
window[windowPropertyName] = report;
}
}

function transformReport([componentName, report]: [
string | undefined,
RenderData,
]) {
return {
name: componentName,
renderCount: report.renders.length,
totalRenderTime: report.time,
};
}

interface RenderData {
count: number;
time: number;
renders: Array<unknown>;
displayName: string | null;
type: React.ComponentType<unknown> | null;
}

type sortedReport = {
name: string | undefined;
renderCount: number;
totalRenderTime: number;
};

// Enable React Scan for performance monitoring - use with dev:react-scan
// Pass in component to get report for specific component
// Pass in interval to get reports at regular intervals
// - useReactScan({ component: LessonPlanDisplay, interval: 10000 });
// When a component is passed it will be added to window object for Playwright testing

export const useReactScan = <T extends object>({
component,
interval,
}: {
component?: React.ComponentType<T>;
interval?: number;
}) => {
useEffect(() => {
const isRenderScanEnabled =
(typeof process !== "undefined" &&
process.env.NEXT_PUBLIC_ENABLE_RENDER_SCAN === "true") ||
(typeof window !== "undefined" &&
window.NEXT_PUBLIC_ENABLE_RENDER_SCAN === "true");

if (isRenderScanEnabled) {
try {
log.info("Initializing React Scan...");
if (typeof window !== "undefined") {
scan({
enabled: true,
log: true,
report: true,
renderCountThreshold: 0,
showToolbar: true,
});
}
log.info("React Scan initialized successfully");
} catch (error) {
log.error("React Scan initialization error:", error);
}

const analyzeRenders = () => {
codeincontext marked this conversation as resolved.
Show resolved Hide resolved
try {
log.info("Attempting to get render reports...");

const allReports = component ? getReport(component) : getReport();

if (
allReports instanceof Map &&
Symbol.iterator in Object(allReports) &&
component === undefined
codeincontext marked this conversation as resolved.
Show resolved Hide resolved
) {
const reportsArray = Array.from(allReports.entries())
.filter(([componentName]) => {
// Exclude styled components and other library-generated components
const isCustomComponent =
// Check if name exists and doesn't start with known library prefixes
componentName &&
!componentName.startsWith("Styled") &&
!componentName.includes("styled") &&
!componentName.startsWith("_") &&
!componentName.includes("$") &&
componentName !== "div" &&
componentName !== "span";

return isCustomComponent;
})
.map(transformReport);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a good candidate for a function to hide these stringy details out of the way and make the hook easier to follow


const sortedReports = reportsArray.toSorted(
(a, b) => b.renderCount - a.renderCount,
);

log.table(sortedReports);
} else if (
allReports !== null &&
allReports instanceof Map === false
) {
const transformedReport = transformReport([
component?.name,
allReports,
]);
log.info("Single Report:,", transformedReport);

setWindowObjectForPlaywright(component, transformedReport);
}
} catch (error) {
log.error("Performance Monitoring Error:", error);
}
};

if (interval) {
const performanceInterval = setInterval(analyzeRenders, interval);

return () => clearInterval(performanceInterval);
} else {
analyzeRenders();
}
}
}, [component, interval]);
};
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
"db-push": "turbo db-push",
"db-push-force": "turbo db-push-force",
"db-seed": "turbo db-seed",
"deps": "NODE_OPTIONS=\"--max-old-space-size=8192\" dependency-cruiser --progress --max-depth 10 --output-type err --config .dependency-cruiser.cjs apps packages",
"deps:graph": "NODE_OPTIONS=\"--max-old-space-size=8192\" dependency-cruiser --progress --max-depth 10 --config .dependency-cruiser.cjs --output-type dot apps packages | dot -T svg > dependency-graph.svg",
"dev": "FORCE_COLOR=1 turbo dev --parallel --ui=stream --log-prefix=none --filter=!@oakai/db",
"dev:react-scan": "NEXT_PUBLIC_ENABLE_RENDER_SCAN=true turbo dev --parallel --ui=stream --log-prefix=none --filter=!@oakai/db",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could this be something along the lines of NEXT_PUBLIC_ENABLE_RENDER_SCAN=true pnpm dev?

"dev:react-scan-monitor": "NEXT_PUBLIC_RENDER_MONITOR=true turbo dev --parallel --ui=stream --log-prefix=none --filter=!@oakai/db",
codeincontext marked this conversation as resolved.
Show resolved Hide resolved
"doppler:pull:dev": "doppler secrets download --config dev --no-file --format env > .env",
"doppler:pull:stg": "doppler secrets download --config stg --no-file --format env > .env",
"doppler:run:stg": "doppler run -c stg --silent",
Expand Down Expand Up @@ -57,7 +57,6 @@
"@semantic-release/git": "^10.0.1",
"@types/jest": "^29.5.14",
"autoprefixer": "^10.4.16",
"dependency-cruiser": "^16.7.0",
"husky": "^8.0.3",
"lint-staged": "^15.2.0",
"next": "14.2.18",
Expand Down
9 changes: 8 additions & 1 deletion packages/logger/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type ChildKey =
| "aila:stream"
| "aila:rag"
| "aila:testing"
| "aila:chat"
| "aila:chat-performance"
Copy link
Collaborator

@codeincontext codeincontext Dec 18, 2024

Choose a reason for hiding this comment

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

aila is for the aila package. Maybe you want performance, chat:performance or ui:performance?

| "aila:experimental-patches"
| "analytics"
| "app"
Expand Down Expand Up @@ -88,10 +88,17 @@ const errorLogger =
*/
export function aiLogger(childKey: ChildKey) {
const debugLogger = debugBase.extend(childKey);

const tableLogger = (tabularData: unknown[], columns?: string[]) => {
if (typeof console !== "undefined" && console.table) {
console.table(tabularData, columns);
}
};
return {
info: debugLogger,
warn: debugLogger,
error: errorLogger.bind(structuredLogger),
table: tableLogger,
};
}

Expand Down
Loading
Loading