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

♻️ [RUM-8123] Introduce a hook to assemble events #3289

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

amortemousque
Copy link
Collaborator

@amortemousque amortemousque commented Jan 16, 2025

Motivation

The overall goal is to create a Modular SDK to simplify contributions and enhance extensibility.

This PR introduces a hook system, initially used to decouple event assembly. Each collection module can now independently handle the assembly of its own properties.

Changes

  • Introduce a Hooks system
  • Use the Assemble hook in viewCollection to manage the assembly of view properties
  • Use the Assemble hook in urlContext to manage the assembly of url properties

Testing

  • Local
  • Staging
  • Unit
  • End to end

I have gone over the contributing documentation.

@amortemousque amortemousque requested a review from a team as a code owner January 16, 2025 19:47
@amortemousque amortemousque marked this pull request as draft January 16, 2025 19:53
@codecov-commenter
Copy link

codecov-commenter commented Jan 16, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 93.67%. Comparing base (c1b94b4) to head (0eb4e78).
Report is 9 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3289      +/-   ##
==========================================
- Coverage   93.68%   93.67%   -0.02%     
==========================================
  Files         288      289       +1     
  Lines        7617     7633      +16     
  Branches     1739     1741       +2     
==========================================
+ Hits         7136     7150      +14     
- Misses        481      483       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link

cit-pr-commenter bot commented Jan 17, 2025

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 145.66 KiB 146.04 KiB 392 B +0.26%
Logs 51.09 KiB 51.09 KiB 0 B 0.00%
Rum Slim 104.45 KiB 104.82 KiB 377 B +0.35%
Worker 24.50 KiB 24.50 KiB 0 B 0.00%
🚀 CPU Performance
Action Name Base Average Cpu Time (ms) Local Average Cpu Time (ms) 𝚫
addglobalcontext 0.002 0.002 0.000
addaction 0.035 0.062 0.027
addtiming 0.001 0.001 -0.000
adderror 0.054 0.106 0.052
startstopsessionreplayrecording 0.012 0.010 -0.002
startview 0.606 0.386 -0.220
logmessage 0.026 0.028 0.002
🧠 Memory Performance
Action Name Base Consumption Memory (bytes) Local Consumption Memory (bytes) 𝚫 (bytes)
addglobalcontext 26.92 KiB 27.68 KiB 779 B
addaction 54.10 KiB 55.24 KiB 1.14 KiB
addtiming 26.43 KiB 24.97 KiB -1500 B
adderror 60.58 KiB 56.94 KiB -3730 B
startstopsessionreplayrecording 25.60 KiB 25.43 KiB -169 B
startview 413.60 KiB 419.60 KiB 5.99 KiB
logmessage 58.94 KiB 57.69 KiB -1281 B

🔗 RealWorld

@amortemousque amortemousque force-pushed the aymeric/introduce-hook-system branch 4 times, most recently from d66a0b9 to 1192e1d Compare January 21, 2025 17:09
@amortemousque amortemousque force-pushed the aymeric/introduce-hook-system branch from 1192e1d to 222c207 Compare January 22, 2025 09:23
@amortemousque amortemousque force-pushed the aymeric/introduce-hook-system branch from e1cef02 to c70139a Compare January 22, 2025 10:41
@amortemousque amortemousque marked this pull request as ready for review January 22, 2025 13:23
): ReturnType<HookCallbackMap[K]> {
const hookCallbacks = callbacks[hookName] || []
const results = hookCallbacks.map((callback) => callback(param))
return combine(...(results as [object, object])) as ReturnType<HookCallbackMap[K]>
Copy link
Contributor

Choose a reason for hiding this comment

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

💬 suggestion: ‏The result looks nice. I'm just a bit concerned about debugging the Assembly Hook for overridden props if we connect many sources to it because we can't easily know the order of declarations. Maybe we could warn user when a prop is being overridden?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I remember we talked about this a bit too, @amortemousque. What do you think of the idea of requiring all hooks to be registered in a single place and specifying them with an array that makes the order clear?

It doesn't look like you actually use the unregister callback returned from register() anywhere, but if there's a need to dynamically add and remove these hooks individually, an alternative approach that might work would be to support enabling and disabling them. That way, they'd keep their initially-defined order, but you'd still be able to turn them on and off when needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree! We discussed this earlier with @sethfowler-datadog, and I experimented with centralized registration. However, this approach adds some boilerplate and coupling, as modules would need to expose their hooks. Self-registration offers better decoupling.

In my view, the assemble hook will be used by contexts and collections, which are explicitly called in startRum. This makes the order relatively easy to visualize. For now, I suggest waiting to see if this becomes an issue before making improvements. Wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

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

For now, I suggest waiting to see if this becomes an issue before making improvements. Wdyt?

That'd be fine with me, if you prefer to take that direction!

... this approach adds some boilerplate and coupling, as modules would need to expose their hooks. Self-registration offers better decoupling.

FWIW, my perspective is that centralized registration does not add any additional coupling; it just turns implicit dependencies, which are hard to reason about, into explicit dependencies, which are easier to reason about. Another way to think about it is that this isn't coupling at all, but composition: we start with small pieces of functionality that can be reasoned about independently, and compose them together in an explicit way to form larger pieces of functionality. You end up with a hierarchical structure, which is nice for all the reasons that structured programming in general is nice.

That said, programming is all about tradeoffs, and there are pros and cons to every solution; I'm totally fine with a dynamic registration approach if you prefer it. As you say, we can always revisit the issue later.

Copy link
Contributor

@sethfowler-datadog sethfowler-datadog left a comment

Choose a reason for hiding this comment

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

I love the direction this takes us in!

): ReturnType<HookCallbackMap[K]> {
const hookCallbacks = callbacks[hookName] || []
const results = hookCallbacks.map((callback) => callback(param))
return combine(...(results as [object, object])) as ReturnType<HookCallbackMap[K]>
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I remember we talked about this a bit too, @amortemousque. What do you think of the idea of requiring all hooks to be registered in a single place and specifying them with an array that makes the order clear?

It doesn't look like you actually use the unregister callback returned from register() anywhere, but if there's a need to dynamically add and remove these hooks individually, an alternative approach that might work would be to support enabling and disabling them. That way, they'd keep their initially-defined order, but you'd still be able to turn them on and off when needed.

packages/rum-core/src/hooks.ts Show resolved Hide resolved
}
export type Hooks = ReturnType<typeof startHooks>

export function startHooks() {
Copy link
Member

@BenoitZugmeyer BenoitZugmeyer Jan 21, 2025

Choose a reason for hiding this comment

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

💬 suggestion: ‏I would rename it createHooks as it doesn't start anything, it just creates a data structure

Comment on lines 158 to 159
service: configuration.service,
version: configuration.version,
Copy link
Member

Choose a reason for hiding this comment

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

💬 suggestion: ‏it might be less confusing / more explicit to have service and version declared in a single place in trackViews.ts:

  const service = viewOptions?.service || configuration.service
  const version = viewOptions?.version || configuration.version

so we can remove those two lines from assembly

packages/rum-core/src/domain/assembly.ts Outdated Show resolved Hide resolved
packages/rum-core/src/hooks.ts Show resolved Hide resolved
@@ -33,11 +38,27 @@ export function startViewCollection(
processViewUpdate(view, configuration, featureFlagContexts, recorderApi, pageStateHistory)
)
)

hooks.register(HookNames.Assemble, ({ startTime, eventType }): PartialRumEvent | undefined => {
Copy link
Member

Choose a reason for hiding this comment

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

🥜 nitpick: ‏you can remove the return type

Suggested change
hooks.register(HookNames.Assemble, ({ startTime, eventType }): PartialRumEvent | undefined => {
hooks.register(HookNames.Assemble, ({ startTime, eventType }) => {

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't know why but it is required for the typechecking to work properly.

Copy link
Member

Choose a reason for hiding this comment

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

Weird. Do you have an example where there is an issue with the return type but not without the return type?

@@ -69,6 +72,18 @@ export function startUrlContexts(
}
}

hooks.register(HookNames.Assemble, ({ startTime, eventType }): PartialRumEvent | undefined => {
Copy link
Member

Choose a reason for hiding this comment

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

🥜 nitpick: ‏same here

Suggested change
hooks.register(HookNames.Assemble, ({ startTime, eventType }): PartialRumEvent | undefined => {
hooks.register(HookNames.Assemble, ({ startTime, eventType }) => {

@amortemousque amortemousque force-pushed the aymeric/introduce-hook-system branch from 02f9fb2 to 0eb4e78 Compare January 23, 2025 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants