Skip to content

Commit

Permalink
feat(scripts): add dynamic workspace packages detection for version w…
Browse files Browse the repository at this point in the history
…arning
  • Loading branch information
pivanov committed Dec 12, 2024
2 parents 156adc9 + 8483e38 commit e6c3d9c
Show file tree
Hide file tree
Showing 22 changed files with 808 additions and 147 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/pkg-pr-new.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Publish Any Commit
on:
push:
branches:
- "**"
pull_request:
branches:
- "**"

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18]

steps:
- uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.1.0

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile --strict-peer-dependencies=false

- name: Build
run: |
cd packages/scan
NODE_ENV=production pnpm build
env:
NODE_ENV: production

- name: Publish NPM Package to pkg-pr-new
run: pnpx pkg-pr-new publish ./packages/scan
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
File renamed without changes.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "root",
"private": true,
"scripts": {
"build": "pnpm --parallel --filter=!extension build",
"build": "WORKSPACE_BUILD=true pnpm --parallel --filter=!extension build && node scripts/version-warning.mjs",
"version-warning": "node scripts/version-warning.mjs",
"dev": "pnpm --parallel --filter=!extension dev",
"pack": "pnpm --parallel --filter=!extension pack",
"pack:bump": "pnpm --filter scan pack:bump",
Expand All @@ -15,6 +16,8 @@
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vercel/style-guide": "^6.0.0",
"boxen": "^8.0.1",
"chalk": "^5.3.0",
"eslint": "^8.57.1",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-jsonc": "^2.18.2",
Expand Down
1 change: 1 addition & 0 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"clean": "rm -rf dist",
"build": "vite build",
"postbuild": "node ../../scripts/version-warning.mjs",
"dev": "pnpm dev:chrome",
"dev:chrome": "cross-env BROWSER=chrome vite",
"dev:firefox": "cross-env BROWSER=firefox vite",
Expand Down
3 changes: 2 additions & 1 deletion packages/scan/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@
],
"scripts": {
"build": "npm run build:css && NODE_ENV=production tsup",
"postbuild": "pnpm copy-astro",
"postbuild": "pnpm copy-astro && node ../../scripts/version-warning.mjs",
"build:copy": "NODE_ENV=production tsup && cat dist/auto.global.js | pbcopy",
"copy-astro": "cp -R src/core/monitor/params/astro dist/core/monitor/params",
"dev:css": "npx tailwindcss -i ./src/core/web/assets/css/styles.tailwind.css -o ./src/core/web/assets/css/styles.css --watch",
Expand All @@ -239,6 +239,7 @@
"@rollup/pluginutils": "^5.1.3",
"@types/node": "^20.17.9",
"bippy": "^0.0.14",
"esbuild": "^0.24.0",
"estree-walker": "^3.0.3",
"kleur": "^4.1.5",
"mri": "^1.2.0",
Expand Down
9 changes: 5 additions & 4 deletions packages/scan/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,13 @@ export type MonitoringOptions = Pick<

interface Monitor {
pendingRequests: number;
url: string | null;
apiKey: string | null;
interactions: Array<InternalInteraction>;
route: string | null;
session: ReturnType<typeof getSession>;
path: string | null;
url: string | null;
route: string | null;
apiKey: string | null;
commit: string | null;
branch: string | null;
}

interface StoreType {
Expand Down
52 changes: 40 additions & 12 deletions packages/scan/src/core/monitor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,63 @@ import { addFiberToSet, isValidFiber, updateFiberRenderData } from '../utils';
import { initPerformanceMonitoring } from './performance';
import { getSession } from './utils';
import { flush } from './network';
import { computeRoute } from './params/utils';

// max retries before the set of components do not get reported (avoid memory leaks of the set of fibers stored on the component aggregation)
const MAX_RETRIES_BEFORE_COMPONENT_GC = 7;

export interface MonitoringProps {
url?: string;
apiKey: string;

// For Session and Interaction
path?: string | null; // pathname (i.e /foo/2/bar/3)
route?: string | null; // computed from path and params (i.e /foo/:fooId/bar/:barId)

// Only used / should be provided to compute the route when using Monitoring without supported framework
params?: Record<string, string>;

// Tracking regressions across commits and branches
commit?: string | null;
branch?: string | null;
}

export type MonitoringWithoutRouteProps = Omit<
MonitoringProps,
'route' | 'path'
>;

export const Monitoring = ({
url,
apiKey,
path,
route,
}: { url?: string; apiKey: string } & {
// todo: ask for path + params so we can compute route for them
path: string;
route: string | null;
}) => {
params,
path = null, // path passed down would be reactive
route = null,
commit = null,
branch = null,
}: MonitoringProps) => {
if (!apiKey)
throw new Error('Please provide a valid API key for React Scan monitoring');
url ??= 'https://monitoring.react-scan.com/api/v1/ingest';

Store.monitor.value ??= {
pendingRequests: 0,
interactions: [],
session: getSession({ commit, branch }).catch(() => null),
url,
apiKey,
interactions: [],
session: getSession().catch(() => null),
route,
path,
commit,
branch,
};
Store.monitor.value.route = route;
Store.monitor.value.path = path;

// When using Monitoring without framework, we need to compute the route from the path and params
if (!route && path && params) {
Store.monitor.value.route = computeRoute(path, params);
} else {
Store.monitor.value.route =
route ?? path ?? new URL(window.location.toString()).pathname; // this is inaccurate on vanilla react if the path is not provided but used for session route
}

useEffect(() => {
scanMonitoring({
Expand Down
143 changes: 57 additions & 86 deletions packages/scan/src/core/monitor/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,29 @@ import {
MAX_PENDING_REQUESTS,
} from './constants';
import { getSession } from './utils';
import type { Interaction, IngestRequest, InternalInteraction } from './types';

const getInteractionId = (interaction: InternalInteraction) =>
`${interaction.performanceEntry.type}::${interaction.componentPath}::${interaction.url}`;
import type { Interaction, IngestRequest, InternalInteraction, Component } from './types';

const INTERACTION_TIME_TILL_COMPLETED = 4000;

const splitInteractions = (interactions: Array<InternalInteraction>) => {
export const flush = async (): Promise<void> => {
const monitor = Store.monitor.value;
if (
!monitor ||
!navigator.onLine ||
!monitor.url ||
!monitor.interactions.length
) {
return;
}
const now = performance.now();
const pendingInteractions: typeof interactions = [];
const completedInteractions: typeof interactions = [];

interactions.forEach((interaction) => {
// We might trigger flush before the interaction is completed,
// so we need to split them into pending and completed by an arbitrary time.
const pendingInteractions = new Array<InternalInteraction>();
const completedInteractions = new Array<InternalInteraction>();

const interactions = monitor.interactions;
for (let i = 0; i < interactions.length; i++) {
const interaction = interactions[i];
if (
now - interaction.performanceEntry.startTime <=
INTERACTION_TIME_TILL_COMPLETED
Expand All @@ -27,102 +37,63 @@ const splitInteractions = (interactions: Array<InternalInteraction>) => {
} else {
completedInteractions.push(interaction);
}
});
}

return { pendingInteractions, completedInteractions };
};
// nothing to flush
if (!completedInteractions.length) return;

const aggregateComponents = (interactions: Array<InternalInteraction>) => {
const aggregatedComponents: Array<{
interactionId: string;
name: string;
renders: number;
instances: number;
totalTime?: number;
selfTime?: number;
}> = [];

for (const interaction of interactions) {
for (const [name, component] of Array.from(
interaction.components.entries(),
)) {
// idempotent
const session = await getSession({
commit: monitor.commit,
branch: monitor.branch,
}).catch(() => null);

if (!session) return;

const aggregatedComponents = new Array<Component>();
const aggregatedInteractions = new Array<Interaction>();
for (let i = 0; i < completedInteractions.length; i++) {
const interaction = completedInteractions[i];

aggregatedInteractions.push({
id: i,
path: interaction.componentPath,
name: interaction.componentName,
time: interaction.performanceEntry.duration,
timestamp: interaction.performanceEntry.timestamp,
type: interaction.performanceEntry.type,
url: interaction.url,
route: interaction.route,
commit: interaction.commit,
branch: interaction.branch,
uniqueInteractionId: interaction.uniqueInteractionId,
});

const components = Array.from(interaction.components.entries());
for (let j = 0; j < components.length; j++) {
const [name, component] = components[j];
aggregatedComponents.push({
name,
instances: component.fibers.size,
interactionId: getInteractionId(interaction),
interactionId: i,
renders: component.renders,
totalTime: component.totalTime,
});

if (component.retiresAllowed === 0) {
// otherwise there will be a memory leak if the user loses internet or our server goes down
// we decide to skip the collection if this is the case
interaction.components.delete(name);
}

component.retiresAllowed -= 1;
}
}
return aggregatedComponents;
};

const toPayloadInteraction = (interactions: Array<InternalInteraction>) =>
interactions.map(
(interaction) =>
({
id: getInteractionId(interaction),
name: interaction.componentName,
time: interaction.performanceEntry.duration,
timestamp: interaction.performanceEntry.timestamp,
type: interaction.performanceEntry.type,
route: interaction.route,
url: interaction.url,
uniqueInteractionId: interaction.uniqueInteractionId,
}) satisfies Interaction,
);

export const flush = async (): Promise<void> => {
const monitor = Store.monitor.value;
if (
!monitor ||
!navigator.onLine ||
!monitor.url ||
!monitor.interactions.length
) {
return;
}
const { completedInteractions, pendingInteractions } = splitInteractions(
monitor.interactions,
);

// nothing to flush
if (!completedInteractions.length) {
return;
}
// idempotent
const session = await getSession().catch(() => null);

if (!session) return;

const aggregatedComponents = aggregateComponents(monitor.interactions);

const payload: IngestRequest = {
interactions: toPayloadInteraction(completedInteractions),
interactions: aggregatedInteractions,
components: aggregatedComponents,
session: {
...session,
url: window.location.toString(),
route: monitor.route, // this might be inaccurate but used to caculate which paths all the unique sessions are coming from without having to join on the interactions table (expensive)
},
};

monitor.pendingRequests++;
// remove all completed interactions from batch
monitor.interactions = monitor.interactions.filter((interaction) =>
completedInteractions.some(
(completedInteraction) =>
completedInteraction.performanceEntry.id !==
interaction.performanceEntry.id,
),
);
monitor.interactions = pendingInteractions;
try {
transport(monitor.url, payload)
.then(() => {
Expand Down
4 changes: 2 additions & 2 deletions packages/scan/src/core/monitor/params/astro/Monitoring.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ interface Props {
const { apiKey, url } = Astro.props;
const pathname = Astro.url.pathname;
const path = Astro.url.pathname;
const params = Astro.params;
---

<AstroMonitor apiKey={apiKey} url={url} pathname={pathname} params={params} client:only="react" />
<AstroMonitor apiKey={apiKey} url={url} path={pathname} params={params} client:only="react" />
8 changes: 4 additions & 4 deletions packages/scan/src/core/monitor/params/astro/component.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createElement } from 'react';
import { Monitoring as BaseMonitoring } from '../..';
import { Monitoring as BaseMonitoring, type MonitoringWithoutRouteProps } from '../..';
import { computeRoute } from '../utils';

export function AstroMonitor(props: {
url?: string;
apiKey: string;
pathname: string;
path: string;
params: Record<string, string>;
}) {
const path = props.pathname;
} & MonitoringWithoutRouteProps) {
const path = props.path;
const route = computeRoute(path, props.params);

return createElement(BaseMonitoring, {
Expand Down
Loading

0 comments on commit e6c3d9c

Please sign in to comment.