-
Notifications
You must be signed in to change notification settings - Fork 33
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
Use only DOMHighResTimeStamp to calculate duration #143
Conversation
src/interfaces.ts
Outdated
target: SpanielTrackedElement; | ||
} | ||
|
||
export interface SpanielObserverEntry extends IntersectionObserverEntryInit { | ||
export interface SpanielRect extends Partial<DOMRectReadOnly> { |
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.
Isn't this just Readonly<Required<DOMRectInit>>
?
And why do we make this change for the duration fix?
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.
The previous code was not accurately typing things. SpanielObserver operates of entries from the intersection observer, which can either be the native intersection observer or the spaniel intersection observer. But the entry was always being typed as the native IntersectionObserverEntry
, whereas the entries from the spaniel intersection observer polyfill are not exactly the same because they are missing a few sub fields. When I addressed this issue, there was a cascading effect. Basically the typings have to respect the fact that the entries could be from native or polyfill IO.
src/spaniel-observer.ts
Outdated
payload: record.payload, | ||
label: state.threshold.label, | ||
entering: false, | ||
rootBounds: emptyRect, | ||
boundingClientRect: boundingClientRect || emptyRect, | ||
intersectionRect: emptyRect, | ||
duration: time - state.lastVisible, | ||
duration: state.lastVisible.highResTime - perfTime, |
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.
perfTime - state.lastVisible.highResTime
? I guess test pass because we calculate opts.visibleTime = entry.unixTime - entry.duration;
later.
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.
oh ya. I'll add a test for the cases where this duration doesn't get overwritten
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.
ok looks like this always gets overwritten anyways. really need to re-write this code.
x: interLeft, | ||
y: interTop, |
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.
Are these new? I might have missed where are x
and y
read?
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.
no, it's just that we previously were not actually returning a https://developer.mozilla.org/en-US/docs/Web/API/DOMRect
so adding it to be closer to the native behavior.
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.
Looks like ClientRect
is a subset of SpanielRect
so there's no need to do ClientRect & SpanielRect
. I also noticed there's built-in DOMRectReadOnly
from https://github.com/microsoft/TypeScript/blob/main/lib/lib.dom.d.ts. So
Line 83 in 6b74e4d
export interface DOMRectReadOnly extends DOMRectInit, DOMMargin {} |
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.
Ok I created export type DOMRectPojo = Omit<DOMRectReadOnly, 'toJSON'>;
Instead of ClientRect & SpanielRect
src/intersection-observer.ts
Outdated
if (el.style.display === 'none') { | ||
return { | ||
unixTime: frame.dateNow, |
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.
Same above. Can we keep only one of time
and unixTime
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.
removed
let { intersectionRatio, rootBounds, boundingClientRect, intersectionRect, isIntersecting, time, target } = entry; | ||
let record = this.recordStore[(<SpanielTrackedElement>target).__spanielId]; | ||
const timeOrigin = w.performance.timeOrigin || w.performance.timing.navigationStart; | ||
const unixTime = this.usingNativeIo ? Math.floor(timeOrigin + time) : time; | ||
const unixTime = this.usingNativeIo |
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 ultimately becomes visibleTime
in the watcher callback right?
We should stick to Date.now
for start time (visibleTime
), and use hr time for calculating the duration. Mixing timeOrigin
/ timing.navigationStart
and Date.now
is not reliable.
Pausing the hr time clock during sleep is okay for us to calculate impression duration.
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.
if we use Date.now
when using native intersection observer, that's also lossy because the callback is async. So capturing a Date.now()
in the callback is not necessarily when the event happened. do you have an opinion on which approach is less bad?
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.
Also, I don't think we are ever mixing timeOrigin
/ timing.navigationStart
and Date.now
for any individual entry (as in, https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry). I would rather use the HR time all throughout the entire entry or Date throughout the entire entry.
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.
The line below, Math.floor(w.performance.timeOrigin || w.performance.timing.navigationStart + time)
is time
unix time? The correct should be hrTime which is relative to timeOrigin. Logic looks incorrect to me.
Left another related comment at line 209.
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.
If the logic is indeed incorrect, do we have a test to cover?
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.
The correct should be hrTime which is relative to timeOrigin
Sort of, it's correct if we want to mirror native IO, but that is a breaking change.
This change just modifies how duration is calculated.
Added tests for visibleTime earlier today: https://github.com/linkedin/spaniel/blob/v2-release/test/playwright/modules/visible-time.ts
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.
Ah wait I see what you mean, how using native IO previously meant using hr time. I will add another test for this behavior first.
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.
wait no we always are using unix timestamp time for the time
field, even for native IO: https://github.com/linkedin/spaniel/blob/v2-release/src/spaniel-observer.ts#L141
So we should keep that behavior for now until a breaking change. I'll add a test regardless
fe2e64d
to
20448a0
Compare
20448a0
to
3872c13
Compare
record.thresholdStates.forEach((state: SpanielThresholdState) => { | ||
const boundingClientRect = record.lastSeenEntry && record.lastSeenEntry.boundingClientRect; | ||
this.handleThresholdExiting( | ||
{ | ||
intersectionRatio: -1, | ||
isIntersecting: false, | ||
unixTime: time, | ||
time, |
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.
Looks like this is not used anywhere anymore, can we remove?
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.
While it's true that it's technically not needed anymore (because time
is a unix timestamp), in next major version, I want to make time
the high res timestamp to mirror the native observer API as closely as possible, get rid of the highRes property, and keep the unixTime property if you need unix time (which we do need a unix timestamp for visibleTime
.
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.
Added tests for visibleTime earlier today: https://github.com/linkedin/spaniel/blob/v2-release/test/playwright/modules/visible-time-tests.ts
spanielEntry.entering = false; | ||
state.visible = false; | ||
this.queuedEntries.push(spanielEntry); | ||
} | ||
|
||
clearTimeout(state.timeoutId); | ||
} | ||
private handleObserverEntry(entry: IntersectionObserverEntry) { | ||
private handleObserverEntry(entry: InternalIntersectionObserverEntry) { |
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'm not sure about this.
handleObserverEntry
is bound as IO callback, and receives hrTime, why switch to use InternalIntersectionObserverEntry
which has extra fields? And it seems we generates unix time inside spaniel, but internalCallback
will pass in hrTime.
let { intersectionRatio, rootBounds, boundingClientRect, intersectionRect, isIntersecting, time, target } = entry; | ||
let record = this.recordStore[(<SpanielTrackedElement>target).__spanielId]; | ||
const timeOrigin = w.performance.timeOrigin || w.performance.timing.navigationStart; | ||
const unixTime = this.usingNativeIo ? Math.floor(timeOrigin + time) : time; | ||
const unixTime = this.usingNativeIo |
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.
The line below, Math.floor(w.performance.timeOrigin || w.performance.timing.navigationStart + time)
is time
unix time? The correct should be hrTime which is relative to timeOrigin. Logic looks incorrect to me.
Left another related comment at line 209.
let { intersectionRatio, rootBounds, boundingClientRect, intersectionRect, isIntersecting, time, target } = entry; | ||
let record = this.recordStore[(<SpanielTrackedElement>target).__spanielId]; | ||
const timeOrigin = w.performance.timeOrigin || w.performance.timing.navigationStart; | ||
const unixTime = this.usingNativeIo ? Math.floor(timeOrigin + time) : time; | ||
const unixTime = this.usingNativeIo |
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.
If the logic is indeed incorrect, do we have a test to cover?
4f4c383
to
23a148b
Compare
23a148b
to
ab5182c
Compare
Previously, we were combining Date.now() and DOMHighResTimeStamp values to calculate duration. These two value types use different clocks, so we should avoid combining the two value types where possible, since using two different clocks leaves us vulnerable to asymetrical issues. The native intersection observer uses DOMHighResTimeStamp, so we should use that type wherever possible in calculations. This change also removes the native-* files, which are superseded by USE_NATIVE_IO and never part of the public API. Thanks to @xg-wang for pointing out some potential issues affecting one clock and not the other, which could in turn cause asymmetrical bugs: w3c/hr-time#115 mdn/content#4713
ab5182c
to
40c9d9e
Compare
Previously, we were combining Date.now() and DOMHighResTimeStamp values to calculate duration. These two value types use different clocks, so we should avoid combining the two value types where possible, since using two different clocks leaves us vulnerable to asymetrical issues. The native intersection observer uses DOMHighResTimeStamp, so we should use that type wherever possible in calculations.
This change also starts running the tests with and without native intersection observer. Previously we were not running tests with native intersection observer.
This change also removes the
native-*
files, which are superseded byUSE_NATIVE_IO
and never part of the public API.Thanks to @xg-wang for pointing out some potential issues affecting one clock and not the other, which could in turn cause asymmetrical bugs:
w3c/hr-time#115
mdn/content#4713