-
Notifications
You must be signed in to change notification settings - Fork 183
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
Replays for React Scan #200
base: main
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
@@ -1,8 +1,14 @@ | |||
{ | |||
"name": "react-scan", | |||
"version": "0.0.54", | |||
"version": "0.0.1083", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this was incremented every time i tested a new change...
* | ||
* handles tracking event timings for arbitrarily overlapping handlers with cancel logic | ||
*/ | ||
export const setupDetailedPointerTimingListener = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where the magic happens
!domFiber || | ||
!domFiber.stateNode || | ||
!(domFiber.stateNode instanceof Element) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
De Morgan's Law 😉
}; | ||
|
||
export const startNewRecording = async () => { | ||
console.log('new recording start'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this PR is not in a reviewable state yet (there are probably on the order of 1000s comments that could be left)
only opening it for visibility
|
||
let lastFiberId = 0; | ||
const fiberIdMap = new Map<Fiber, number>(); | ||
const getFiberId = (fiber: Fiber) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how different is this from bippy's getFiberID
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think i wrote that before it existed
// "first rrweb event must be meta event" | ||
// ); | ||
if (payload.events[0].type !== 4) { | ||
console.warn('hm', payload); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we probably need a DEV flag here now lol, perhaps import.meta.env.DEV
f4de562
to
90328f3
Compare
818ac8d
to
c6456e9
Compare
collect detailed browser timing on interaction implement react scan rrweb replayer plugin refactor monitoring interaction and component collection
c6456e9
to
fd0b84e
Compare
@@ -1,3 +1,5 @@ | |||
import 'bippy'; // implicit init RDT hook | |||
|
|||
export * from './core/index'; | |||
|
|||
export { ReactScanReplayPlugin } from './core//monitor/session-replay/replay-v2'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we remove the extra /
here?
channels: Record< | ||
ChanelName, | ||
{ callbacks: BoundedArray<Callback>; state: BoundedArray<Item> } | ||
>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can do something like this, and use generics here.
channels: Record< | |
ChanelName, | |
{ callbacks: BoundedArray<Callback>; state: BoundedArray<Item> } | |
>; | |
channels: Record<ChannelName, Channel<T>>; |
and you could create an interface for it like this..
interface Channel<T> {
callbacks: BoundedArray<Callback<T>>;
state: BoundedArray<T>;
}
type PerformanceEntryChannelsType = { | ||
subscribe: (to: ChanelName, cb: Callback) => UnSubscribe; | ||
publish: ( | ||
item: Item, | ||
to: ChanelName, | ||
dropFirst: boolean, | ||
createIfNoChannel: boolean, | ||
) => void; | ||
channels: Record< | ||
ChanelName, | ||
{ callbacks: BoundedArray<Callback>; state: BoundedArray<Item> } | ||
>; | ||
getAvailableChannels: () => BoundedArray<string>; | ||
updateChannelState: ( | ||
channel: ChanelName, | ||
updater: Updater, | ||
createIfNoChannel: boolean, | ||
) => void; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a little more strongly typed here.. and you can use the generics when you implement PerformanceEntryChannelsType
type PerformanceEntryChannelsType = { | |
subscribe: (to: ChanelName, cb: Callback) => UnSubscribe; | |
publish: ( | |
item: Item, | |
to: ChanelName, | |
dropFirst: boolean, | |
createIfNoChannel: boolean, | |
) => void; | |
channels: Record< | |
ChanelName, | |
{ callbacks: BoundedArray<Callback>; state: BoundedArray<Item> } | |
>; | |
getAvailableChannels: () => BoundedArray<string>; | |
updateChannelState: ( | |
channel: ChanelName, | |
updater: Updater, | |
createIfNoChannel: boolean, | |
) => void; | |
}; | |
export type ChannelItem = InternalInteraction | PerformanceInteraction; | |
export type UnSubscribe = () => void; | |
export type Callback<T> = (item: T) => void; | |
export type Updater<T> = (state: BoundedArray<T>) => BoundedArray<T>; | |
export type ChannelName = string; | |
interface Channel<T> { | |
callbacks: BoundedArray<Callback<T>>; | |
state: BoundedArray<T>; | |
} | |
interface PerformanceEntryChannelsType<T> { | |
subscribe: (to: ChannelName, cb: Callback<T>, dropFirst?: boolean) => UnSubscribe; | |
publish: ( | |
item: T, | |
to: ChannelName, | |
createIfNoChannel?: boolean | |
) => void; | |
channels: Record<ChannelName, Channel<T>>; | |
getAvailableChannels: () => BoundedArray<string>; | |
updateChannelState: ( | |
channel: ChannelName, | |
updater: Updater<T>, | |
createIfNoChannel?: boolean | |
) => void; | |
getChannelState: (channel: ChannelName) => BoundedArray<T>; | |
} |
class PerformanceEntryChannels implements PerformanceEntryChannelsType { | ||
channels: PerformanceEntryChannelsType['channels'] = {}; | ||
publish(item: Item, to: ChanelName, createIfNoChannel = true) { | ||
const existingChannel = this.channels[to]; | ||
if (!existingChannel) { | ||
if (!createIfNoChannel) { | ||
return; | ||
} | ||
this.channels[to] = { | ||
callbacks: new BoundedArray<Callback>(MAX_CHANNEL_SIZE), | ||
state: new BoundedArray<Item>(MAX_CHANNEL_SIZE), | ||
}; | ||
this.channels[to].state.push(item); | ||
return; | ||
} | ||
|
||
existingChannel.state.push(item); | ||
existingChannel.callbacks.forEach((cb) => cb(item)); | ||
} | ||
|
||
getAvailableChannels() { | ||
return BoundedArray.fromArray(Object.keys(this.channels), MAX_CHANNEL_SIZE); | ||
} | ||
subscribe(to: ChanelName, cb: Callback, dropFirst: boolean = false) { | ||
const defer = () => { | ||
if (!dropFirst) { | ||
this.channels[to].state.forEach((item) => { | ||
cb(item); | ||
}); | ||
} | ||
return () => { | ||
const filtered = this.channels[to].callbacks.filter( | ||
(subscribed) => subscribed !== cb | ||
); | ||
this.channels[to].callbacks = BoundedArray.fromArray(filtered, MAX_CHANNEL_SIZE); | ||
}; | ||
}; | ||
const existing = this.channels[to]; | ||
if (!existing) { | ||
this.channels[to] = { | ||
callbacks: new BoundedArray<Callback>(MAX_CHANNEL_SIZE), | ||
state: new BoundedArray<Item>(MAX_CHANNEL_SIZE), | ||
}; | ||
this.channels[to].callbacks.push(cb); | ||
return defer(); | ||
} | ||
|
||
existing.callbacks.push(cb); | ||
return defer(); | ||
} | ||
updateChannelState( | ||
channel: ChanelName, | ||
updater: Updater, | ||
createIfNoChannel = true, | ||
) { | ||
const existingChannel = this.channels[channel]; | ||
if (!existingChannel) { | ||
if (!createIfNoChannel) { | ||
return; | ||
} | ||
|
||
const state = new BoundedArray<Item>(MAX_CHANNEL_SIZE) | ||
const newChannel = { callbacks: new BoundedArray<Item>(MAX_CHANNEL_SIZE), state }; | ||
|
||
this.channels[channel] = newChannel; | ||
newChannel.state = updater(state); | ||
return; | ||
} | ||
|
||
existingChannel.state = updater(existingChannel.state); | ||
} | ||
|
||
getChannelState(channel: ChanelName) { | ||
return this.channels[channel].state ?? new BoundedArray<Item>(MAX_CHANNEL_SIZE); | ||
} | ||
} | ||
|
||
export const performanceEntryChannels = new PerformanceEntryChannels(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really great. I'm so excited for this feature.
Fly on the wall here without write access. Happy to help with TypeScript a little I don't step on your toes.
class PerformanceEntryChannels implements PerformanceEntryChannelsType { | |
channels: PerformanceEntryChannelsType['channels'] = {}; | |
publish(item: Item, to: ChanelName, createIfNoChannel = true) { | |
const existingChannel = this.channels[to]; | |
if (!existingChannel) { | |
if (!createIfNoChannel) { | |
return; | |
} | |
this.channels[to] = { | |
callbacks: new BoundedArray<Callback>(MAX_CHANNEL_SIZE), | |
state: new BoundedArray<Item>(MAX_CHANNEL_SIZE), | |
}; | |
this.channels[to].state.push(item); | |
return; | |
} | |
existingChannel.state.push(item); | |
existingChannel.callbacks.forEach((cb) => cb(item)); | |
} | |
getAvailableChannels() { | |
return BoundedArray.fromArray(Object.keys(this.channels), MAX_CHANNEL_SIZE); | |
} | |
subscribe(to: ChanelName, cb: Callback, dropFirst: boolean = false) { | |
const defer = () => { | |
if (!dropFirst) { | |
this.channels[to].state.forEach((item) => { | |
cb(item); | |
}); | |
} | |
return () => { | |
const filtered = this.channels[to].callbacks.filter( | |
(subscribed) => subscribed !== cb | |
); | |
this.channels[to].callbacks = BoundedArray.fromArray(filtered, MAX_CHANNEL_SIZE); | |
}; | |
}; | |
const existing = this.channels[to]; | |
if (!existing) { | |
this.channels[to] = { | |
callbacks: new BoundedArray<Callback>(MAX_CHANNEL_SIZE), | |
state: new BoundedArray<Item>(MAX_CHANNEL_SIZE), | |
}; | |
this.channels[to].callbacks.push(cb); | |
return defer(); | |
} | |
existing.callbacks.push(cb); | |
return defer(); | |
} | |
updateChannelState( | |
channel: ChanelName, | |
updater: Updater, | |
createIfNoChannel = true, | |
) { | |
const existingChannel = this.channels[channel]; | |
if (!existingChannel) { | |
if (!createIfNoChannel) { | |
return; | |
} | |
const state = new BoundedArray<Item>(MAX_CHANNEL_SIZE) | |
const newChannel = { callbacks: new BoundedArray<Item>(MAX_CHANNEL_SIZE), state }; | |
this.channels[channel] = newChannel; | |
newChannel.state = updater(state); | |
return; | |
} | |
existingChannel.state = updater(existingChannel.state); | |
} | |
getChannelState(channel: ChanelName) { | |
return this.channels[channel].state ?? new BoundedArray<Item>(MAX_CHANNEL_SIZE); | |
} | |
} | |
export const performanceEntryChannels = new PerformanceEntryChannels(); | |
class PerformanceEntryChannels<T> implements PerformanceEntryChannelsType<T> { | |
private readonly channels: Record<ChannelName, Channel<T>> = {}; | |
private createChannel(): Channel<T> { | |
return { | |
callbacks: new BoundedArray<Callback<T>>(MAX_CHANNEL_SIZE), | |
state: new BoundedArray<T>(MAX_CHANNEL_SIZE), | |
}; | |
} | |
private ensureChannel(channelName: ChannelName, createIfNoChannel = true): Channel<T> | null { | |
if (!this.channels[channelName]) { | |
if (!createIfNoChannel) { | |
return null; | |
} | |
this.channels[channelName] = this.createChannel(); | |
} | |
return this.channels[channelName]; | |
} | |
public publish(item: T, to: ChannelName, createIfNoChannel = true): void { | |
const channel = this.ensureChannel(to, createIfNoChannel); | |
if (!channel) return; | |
channel.state.push(item); | |
channel.callbacks.forEach((cb) => { | |
try { | |
cb(item); | |
} catch (error) { | |
console.error(`Error in channel "${to}" callback:`, error); | |
} | |
}); | |
} | |
public subscribe(to: ChannelName, cb: Callback<T>, dropFirst = false): UnSubscribe { | |
const channel = this.ensureChannel(to, true); | |
if (!channel) throw new Error(`Failed to create/access channel: ${to}`); | |
channel.callbacks.push(cb); | |
if (!dropFirst) { | |
channel.state.forEach((item) => { | |
try { | |
cb(item); | |
} catch (error) { | |
console.error(`Error in channel "${to}" subscription callback:`, error); | |
} | |
}); | |
} | |
return () => { | |
const filtered = channel.callbacks.filter( | |
(subscribed) => subscribed !== cb | |
); | |
channel.callbacks = BoundedArray.fromArray(filtered, MAX_CHANNEL_SIZE); | |
}; | |
} | |
public getAvailableChannels(): BoundedArray<string> { | |
return BoundedArray.fromArray(Object.keys(this.channels), MAX_CHANNEL_SIZE); | |
} | |
public updateChannelState( | |
channelName: ChannelName, | |
updater: Updater<T>, | |
createIfNoChannel = true | |
): void { | |
const channel = this.ensureChannel(channelName, createIfNoChannel); | |
if (!channel) return; | |
try { | |
channel.state = updater(channel.state); | |
} catch (error) { | |
console.error(`Error updating channel "${channelName}" state:`, error); | |
} | |
} | |
public getChannelState(channelName: ChannelName): BoundedArray<T> { | |
const channel = this.channels[channelName]; | |
return channel?.state ?? new BoundedArray<T>(MAX_CHANNEL_SIZE); | |
} | |
public clearChannel(channelName: ChannelName): void { | |
const channel = this.channels[channelName]; | |
if (channel) { | |
channel.state = new BoundedArray<T>(MAX_CHANNEL_SIZE); | |
} | |
} | |
public removeChannel(channelName: ChannelName): void { | |
delete this.channels[channelName]; | |
} | |
public getSubscriberCount(channelName: ChannelName): number { | |
return this.channels[channelName]?.callbacks.length ?? 0; | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the contributions, for context I will probably be migrating this logic over for a react-scan feature (some harder backend problems preventing this from being immediately implemented)
whatever changes i bring over, ill do my best to give attribution :)
This PR Introduces a runtime for react scan to:
Other changes include:
What the PR does not include is the infrastructure behind replays for react scan, that will be open sourced at a future date
That functionaliy roughly includes:
Preview of visualized collected data
demo-video-3-compressed.mp4