Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/PostHog/posthog-js into boo…
Browse files Browse the repository at this point in the history
…tstrapped-flag-evt-property
  • Loading branch information
havenbarnes committed Dec 5, 2024
2 parents c4b0c0e + 909eda0 commit de0ad0a
Show file tree
Hide file tree
Showing 25 changed files with 420 additions and 112 deletions.
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,40 @@
## 1.194.3 - 2024-12-03

- fix: parse Chrome instead of Android as browser (#1575)

## 1.194.2 - 2024-12-02

- fix: use previous site-app variables (#1574)

## 1.194.1 - 2024-11-30

- fix: Don't crash on bigints (#1573)
- feat: Add customization to print events drop them (#1572)

## 1.194.0 - 2024-11-29

- feat: add $recording_status property (#1571)

## 1.193.1 - 2024-11-28

- fix: zone detection (#1570)

## 1.193.0 - 2024-11-28

- feat: allow decide to provide script name for recorder (#1509)

## 1.192.1 - 2024-11-28



## 1.192.0 - 2024-11-28

- feat: Start tracking timezone offset and language prefix (#1568)

## 1.191.0 - 2024-11-28

- feat: different default and max idle period (#1558)

## 1.190.2 - 2024-11-27

- fix: patch to angular detection in rrweb (#1566)
Expand Down
1 change: 1 addition & 0 deletions cypress/e2e/session-recording.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ describe('Session recording', () => {
capturedSnapshot['properties']['$snapshot_data'][3],
capturedSnapshot['properties']['$snapshot_data'][4],
])

expectPageViewCustomEvent(customEvents[0])
expectPostHogConfigCustomEvent(customEvents[1])
expectSessionOptionsCustomEvent(customEvents[2])
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "posthog-js",
"version": "1.190.2",
"version": "1.194.3",
"description": "Posthog-js allows you to automatically capture usage and send events to PostHog.",
"repository": "https://github.com/PostHog/posthog-js",
"author": "[email protected]",
Expand Down
37 changes: 16 additions & 21 deletions patches/@[email protected]
Original file line number Diff line number Diff line change
@@ -1,37 +1,32 @@
diff --git a/dist/record.js b/dist/record.js
index 46ec389fefb698243008b39db65470dbdf0a3857..70db907755d68b08232e25e1b255a974f56f3c65 100644
index 46ec389fefb698243008b39db65470dbdf0a3857..a18724d8b6ba43a30935daf257127fbb0c898541 100644
--- a/dist/record.js
+++ b/dist/record.js
@@ -26,6 +26,19 @@ const testableMethods$1 = {
@@ -26,6 +26,14 @@ const testableMethods$1 = {
Element: [],
MutationObserver: ["constructor"]
};
+const isFunction = (x) => typeof x === 'function';
+const isAngularZonePatchedFunction = (x) => {
+
+const isAngularZonePresent = () => {
+ try {
+ if (!isFunction(x)) {
+ return false;
+ }
+ const prototypeKeys = Object.getOwnPropertyNames(x.prototype || {});
+ return prototypeKeys.some((key) => key.indexOf('__zone'));
+ return !!globalThis.Zone
+ } catch {
+ // we've seen some intermittent problems in Safari since introducing this check
+ return false
+ }
+}
const untaintedBasePrototype$1 = {};
function getUntaintedPrototype$1(key) {
if (untaintedBasePrototype$1[key])
@@ -54,7 +67,7 @@ function getUntaintedPrototype$1(key) {
@@ -54,7 +62,7 @@ function getUntaintedPrototype$1(key) {
}
)
);
- if (isUntaintedAccessors && isUntaintedMethods) {
+ if (isUntaintedAccessors && isUntaintedMethods && !isAngularZonePatchedFunction(defaultObj)) {
+ if (isUntaintedAccessors && isUntaintedMethods && !isAngularZonePresent()) {
untaintedBasePrototype$1[key] = defaultObj.prototype;
return defaultObj.prototype;
}
@@ -65,10 +78,10 @@ function getUntaintedPrototype$1(key) {
@@ -65,10 +73,10 @@ function getUntaintedPrototype$1(key) {
if (!win) return defaultObj.prototype;
const untaintedObject = win[key].prototype;
document.body.removeChild(iframeEl);
Expand All @@ -44,7 +39,7 @@ index 46ec389fefb698243008b39db65470dbdf0a3857..70db907755d68b08232e25e1b255a974
}
}
const untaintedAccessorCache$1 = {};
@@ -246,6 +259,9 @@ function isCSSImportRule(rule2) {
@@ -246,6 +254,9 @@ function isCSSImportRule(rule2) {
function isCSSStyleRule(rule2) {
return "selectorText" in rule2;
}
Expand All @@ -54,7 +49,7 @@ index 46ec389fefb698243008b39db65470dbdf0a3857..70db907755d68b08232e25e1b255a974
class Mirror {
constructor() {
__publicField$1(this, "idNodeMap", /* @__PURE__ */ new Map());
@@ -809,9 +825,14 @@ function serializeElementNode(n2, options) {
@@ -809,9 +820,14 @@ function serializeElementNode(n2, options) {
}
}
if (tagName === "link" && inlineStylesheet) {
Expand All @@ -72,7 +67,7 @@ index 46ec389fefb698243008b39db65470dbdf0a3857..70db907755d68b08232e25e1b255a974
let cssText = null;
if (stylesheet) {
cssText = stringifyStylesheet(stylesheet);
@@ -855,7 +876,15 @@ function serializeElementNode(n2, options) {
@@ -855,7 +871,15 @@ function serializeElementNode(n2, options) {
}
}
if (tagName === "dialog" && n2.open) {
Expand All @@ -89,7 +84,7 @@ index 46ec389fefb698243008b39db65470dbdf0a3857..70db907755d68b08232e25e1b255a974
}
if (tagName === "canvas" && recordCanvas) {
if (n2.__context === "2d") {
@@ -1116,7300 +1145,227 @@ function serializeNodeWithId(n2, options) {
@@ -1116,7300 +1140,227 @@ function serializeNodeWithId(n2, options) {
keepIframeSrcFn
};
if (serializedNode.type === NodeType$2.Element && serializedNode.tagName === "textarea" && serializedNode.attributes.value !== void 0) ;
Expand Down Expand Up @@ -7599,16 +7594,16 @@ index 46ec389fefb698243008b39db65470dbdf0a3857..70db907755d68b08232e25e1b255a974
class BaseRRNode {
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
constructor(..._args) {
@@ -8507,7 +1463,7 @@ function getUntaintedPrototype(key) {
@@ -8507,7 +1458,7 @@ function getUntaintedPrototype(key) {
}
)
);
- if (isUntaintedAccessors && isUntaintedMethods) {
+ if (isUntaintedAccessors && isUntaintedMethods && !isAngularZonePatchedFunction(defaultObj)) {
+ if (isUntaintedAccessors && isUntaintedMethods && !isAngularZonePresent()) {
untaintedBasePrototype[key] = defaultObj.prototype;
return defaultObj.prototype;
}
@@ -11382,11 +4338,19 @@ class CanvasManager {
@@ -11382,11 +4333,19 @@ class CanvasManager {
let rafId;
const getCanvas = () => {
const matchedCanvas = [];
Expand All @@ -7633,7 +7628,7 @@ index 46ec389fefb698243008b39db65470dbdf0a3857..70db907755d68b08232e25e1b255a974
return matchedCanvas;
};
const takeCanvasSnapshots = (timestamp) => {
@@ -11407,13 +4371,20 @@ class CanvasManager {
@@ -11407,13 +4366,20 @@ class CanvasManager {
context.clear(context.COLOR_BUFFER_BIT);
}
}
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

111 changes: 94 additions & 17 deletions src/__tests__/extensions/replay/sessionrecording.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,7 @@ describe('SessionRecording', () => {
let sessionIdGeneratorMock: Mock
let windowIdGeneratorMock: Mock
let onFeatureFlagsCallback: ((flags: string[], variants: Record<string, string | boolean>) => void) | null
let removeCaptureHookMock: Mock
let addCaptureHookMock: Mock
let removePageviewCaptureHookMock: Mock
let simpleEventEmitter: SimpleEventEmitter

const addRRwebToWindow = () => {
Expand All @@ -208,6 +207,7 @@ describe('SessionRecording', () => {
}

beforeEach(() => {
removePageviewCaptureHookMock = jest.fn()
sessionId = 'sessionId' + uuidv7()

config = {
Expand Down Expand Up @@ -235,11 +235,11 @@ describe('SessionRecording', () => {
const postHogPersistence = new PostHogPersistence(config)
postHogPersistence.clear()

sessionManager = new SessionIdManager(config, postHogPersistence, sessionIdGeneratorMock, windowIdGeneratorMock)

// add capture hook returns an unsubscribe function
removeCaptureHookMock = jest.fn()
addCaptureHookMock = jest.fn().mockImplementation(() => removeCaptureHookMock)
sessionManager = new SessionIdManager(
{ config, persistence: postHogPersistence, register: jest.fn() } as unknown as PostHog,
sessionIdGeneratorMock,
windowIdGeneratorMock
)

simpleEventEmitter = new SimpleEventEmitter()
// TODO we really need to make this a real posthog instance :cry:
Expand All @@ -258,17 +258,17 @@ describe('SessionRecording', () => {
},
sessionManager: sessionManager,
requestRouter: new RequestRouter({ config } as any),
_addCaptureHook: addCaptureHookMock,
consent: {
isOptedOut(): boolean {
return false
},
} as unknown as ConsentManager,
register_for_session() {},
_internalEventEmitter: simpleEventEmitter,
on: (event, cb) => {
return simpleEventEmitter.on(event, cb)
},
on: jest.fn().mockImplementation((event, cb) => {
const unsubscribe = simpleEventEmitter.on(event, cb)
return removePageviewCaptureHookMock.mockImplementation(unsubscribe)
}),
} as Partial<PostHog> as PostHog

loadScriptMock.mockImplementation((_ph, _path, callback) => {
Expand Down Expand Up @@ -419,21 +419,21 @@ describe('SessionRecording', () => {
sessionRecording.startIfEnabledOrStop()

expect(sessionRecording['_removePageViewCaptureHook']).not.toBeUndefined()
expect(posthog._addCaptureHook).toHaveBeenCalledTimes(1)
expect(posthog.on).toHaveBeenCalledTimes(1)

// calling a second time doesn't add another capture hook
sessionRecording.startIfEnabledOrStop()
expect(posthog._addCaptureHook).toHaveBeenCalledTimes(1)
expect(posthog.on).toHaveBeenCalledTimes(1)
})

it('removes the pageview capture hook on stop', () => {
sessionRecording.startIfEnabledOrStop()
expect(sessionRecording['_removePageViewCaptureHook']).not.toBeUndefined()

expect(removeCaptureHookMock).not.toHaveBeenCalled()
expect(removePageviewCaptureHookMock).not.toHaveBeenCalled()
sessionRecording.stopRecording()

expect(removeCaptureHookMock).toHaveBeenCalledTimes(1)
expect(removePageviewCaptureHookMock).toHaveBeenCalledTimes(1)
expect(sessionRecording['_removePageViewCaptureHook']).toBeUndefined()
})

Expand Down Expand Up @@ -485,6 +485,15 @@ describe('SessionRecording', () => {
jest.spyOn(sessionRecording, 'startIfEnabledOrStop')
})

it('loads script based on script config', () => {
sessionRecording.afterDecideResponse(
makeDecideResponse({
sessionRecording: { endpoint: '/s/', scriptConfig: { script: 'experimental-recorder' } },
})
)
expect(loadScriptMock).toHaveBeenCalledWith(posthog, 'experimental-recorder', expect.any(Function))
})

it('when the first event is a meta it does not take a manual full snapshot', () => {
sessionRecording.startIfEnabledOrStop()
expect(loadScriptMock).toHaveBeenCalled()
Expand Down Expand Up @@ -1130,7 +1139,11 @@ describe('SessionRecording', () => {
let unsubscribeCallback: () => void

beforeEach(() => {
sessionManager = new SessionIdManager(config, new PostHogPersistence(config))
sessionManager = new SessionIdManager({
config,
persistence: new PostHogPersistence(config),
register: jest.fn(),
} as unknown as PostHog)
posthog.sessionManager = sessionManager

mockCallback = jest.fn()
Expand Down Expand Up @@ -1216,7 +1229,11 @@ describe('SessionRecording', () => {

describe('with a real session id manager', () => {
beforeEach(() => {
sessionManager = new SessionIdManager(config, new PostHogPersistence(config))
sessionManager = new SessionIdManager({
config,
persistence: new PostHogPersistence(config),
register: jest.fn(),
} as unknown as PostHog)
posthog.sessionManager = sessionManager

sessionRecording.startIfEnabledOrStop()
Expand Down Expand Up @@ -1762,6 +1779,66 @@ describe('SessionRecording', () => {
expect(sessionRecording['_linkedFlagSeen']).toEqual(true)
expect(sessionRecording['status']).toEqual('active')
})

/**
* this is partly a regression test, with a running rrweb,
* if you don't pause while buffering
* the browser can be trapped in an infinite loop of pausing
* while trying to report it is paused 🙈
*/
it('can be paused while waiting for flag', () => {
fakeNavigateTo('https://test.com/blocked')

expect(sessionRecording['_linkedFlag']).toEqual(null)
expect(sessionRecording['_linkedFlagSeen']).toEqual(false)
expect(sessionRecording['status']).toEqual('buffering')

sessionRecording.afterDecideResponse(
makeDecideResponse({
sessionRecording: {
endpoint: '/s/',
linkedFlag: 'the-flag-key',
urlBlocklist: [
{
matching: 'regex',
url: '/blocked',
},
],
},
})
)

expect(sessionRecording['_linkedFlag']).toEqual('the-flag-key')
expect(sessionRecording['_linkedFlagSeen']).toEqual(false)
expect(sessionRecording['status']).toEqual('buffering')
expect(sessionRecording['paused']).toBeUndefined()

const snapshotEvent = {
event: 123,
type: INCREMENTAL_SNAPSHOT_EVENT_TYPE,
data: {
source: 1,
},
timestamp: new Date().getTime(),
}
_emit(snapshotEvent)

expect(sessionRecording['_linkedFlag']).toEqual('the-flag-key')
expect(sessionRecording['_linkedFlagSeen']).toEqual(false)
expect(sessionRecording['status']).toEqual('paused')

sessionRecording.overrideLinkedFlag()

expect(sessionRecording['_linkedFlagSeen']).toEqual(true)
expect(sessionRecording['status']).toEqual('paused')

fakeNavigateTo('https://test.com/allowed')

expect(sessionRecording['status']).toEqual('paused')

_emit(snapshotEvent)
expect(sessionRecording['status']).toEqual('active')
})
})

describe('buffering minimum duration', () => {
Expand Down
Loading

0 comments on commit de0ad0a

Please sign in to comment.