Skip to content

[RUM-9181]supporting logs in browser extensions #3455

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

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/core/src/browser/addEventListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,10 @@ export function addEventListeners<Target extends EventTarget, EventName extends
const options = passive ? { capture, passive } : capture

// Use the window.EventTarget.prototype when possible to avoid wrong overrides (e.g: https://github.com/salesforce/lwc/issues/1824)
const listenerTarget =
window.EventTarget && eventTarget instanceof EventTarget ? window.EventTarget.prototype : eventTarget
const listenerTarget =
typeof window !== 'undefined' && window.EventTarget && eventTarget instanceof EventTarget
? window.EventTarget.prototype
: eventTarget

const add = getZoneJsOriginalValue(listenerTarget, 'addEventListener')
eventNames.forEach((eventName) => add.call(eventTarget, eventName, listenerWithMonitor, options))
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/browser/cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export interface CookieOptions {
}

export function setCookie(name: string, value: string, expireDelay: number = 0, options?: CookieOptions) {
if (typeof document === 'undefined') {
return
}

const date = new Date()
date.setTime(date.getTime() + expireDelay)
const expires = `expires=${date.toUTCString()}`
Expand All @@ -21,6 +25,10 @@ export function setCookie(name: string, value: string, expireDelay: number = 0,
}

export function getCookie(name: string) {
if (typeof document === 'undefined') {
return undefined
}

return findCommaSeparatedValue(document.cookie, name)
}

Expand All @@ -31,6 +39,10 @@ let initCookieParsed: Map<string, string> | undefined
* to avoid accessing document.cookie multiple times.
*/
export function getInitCookie(name: string) {
if (typeof document === 'undefined' || !document.cookie) {
return undefined
}

if (!initCookieParsed) {
initCookieParsed = findCommaSeparatedValues(document.cookie)
}
Expand All @@ -46,6 +58,10 @@ export function deleteCookie(name: string, options?: CookieOptions) {
}

export function areCookiesAuthorized(options: CookieOptions): boolean {
if (typeof document === 'undefined') {
return false
}

if (document.cookie === undefined || document.cookie === null) {
return false
}
Expand All @@ -71,6 +87,10 @@ export function areCookiesAuthorized(options: CookieOptions): boolean {
*/
let getCurrentSiteCache: string | undefined
export function getCurrentSite() {
if (typeof document === 'undefined' || typeof window === 'undefined' || !window.location) {
return ''
}

if (getCurrentSiteCache === undefined) {
// Use a unique cookie name to avoid issues when the SDK is initialized multiple times during
// the test cookie lifetime
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/browser/fetchObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Observable } from '../tools/observable'
import type { ClocksState } from '../tools/utils/timeUtils'
import { clocksNow } from '../tools/utils/timeUtils'
import { normalizeUrl } from '../tools/utils/urlPolyfill'
import { getGlobalObject } from '../tools/getGlobalObject'

interface FetchContextBase {
method: string
Expand Down Expand Up @@ -45,11 +46,13 @@ export function resetFetchObservable() {

function createFetchObservable() {
return new Observable<FetchContext>((observable) => {
if (!window.fetch) {
const globalObject = getGlobalObject<typeof globalThis>()

if (!('fetch' in globalObject)) {
return
}

const { stop } = instrumentMethod(window, 'fetch', (call) => beforeSend(call, observable), {
const { stop } = instrumentMethod(globalObject, 'fetch', (call) => beforeSend(call, observable), {
computeHandlingStack: true,
})

Expand All @@ -58,7 +61,7 @@ function createFetchObservable() {
}

function beforeSend(
{ parameters, onPostCall, handlingStack }: InstrumentedMethodCall<Window, 'fetch'>,
{ parameters, onPostCall, handlingStack }: InstrumentedMethodCall<typeof globalThis, 'fetch'>,
observable: Observable<FetchContext>
) {
const [input, init] = parameters
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/browser/xhrObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { normalizeUrl } from '../tools/utils/urlPolyfill'
import { shallowClone } from '../tools/utils/objectUtils'
import type { Configuration } from '../domain/configuration'
import { addEventListener } from './addEventListener'
import { getGlobalObject } from '../tools/getGlobalObject'

export interface XhrOpenContext {
state: 'open'
Expand Down Expand Up @@ -42,6 +43,11 @@ export function initXhrObservable(configuration: Configuration) {

function createXhrObservable(configuration: Configuration) {
return new Observable<XhrContext>((observable) => {
const globalObject = getGlobalObject<typeof globalThis>()
if (!('XMLHttpRequest' in globalObject)) {
return
}

const { stop: stopInstrumentingStart } = instrumentMethod(XMLHttpRequest.prototype, 'open', openXhr)

const { stop: stopInstrumentingSend } = instrumentMethod(
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/domain/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface InitConfiguration {
silentMultipleInit?: boolean | undefined

/**
* Which storage strategy to use for persisting sessions. Can be either 'cookie' or 'local-storage'.
* Which storage strategy to use for persisting sessions. Can be either 'cookie', 'local-storage', or 'service-worker'.
*
* Important: If you are using the RUM and Logs Browser SDKs, this option must be configured with identical values
* @default "cookie"
Expand Down
25 changes: 22 additions & 3 deletions packages/core/src/domain/connectivity/connectivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,36 @@ export interface NetworkInformation {
}

export interface Connectivity {
status: 'connected' | 'not_connected'
status: 'connected' | 'not_connected' | 'maybe'
interfaces?: NetworkInterface[]
effective_type?: EffectiveType
[key: string]: unknown
}

export function getConnectivity(): Connectivity {
const navigator = window.navigator as BrowserNavigator
const isServiceWorker = typeof self !== 'undefined' && 'ServiceWorkerGlobalScope' in self;

const isBrowser = typeof window !== 'undefined';

if (!isBrowser && !isServiceWorker) {
return {
status: 'not_connected',
};
}

const globalNav = isServiceWorker ? self.navigator : window.navigator;

const navigator = globalNav as BrowserNavigator;

if (!navigator) {
return {
status: 'not_connected',
};
}

return {
status: navigator.onLine ? 'connected' : 'not_connected',
interfaces: navigator.connection && navigator.connection.type ? [navigator.connection.type] : undefined,
effective_type: navigator.connection?.effectiveType,
}
};
}
17 changes: 15 additions & 2 deletions packages/core/src/domain/error/trackRuntimeError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { computeStackTrace, computeStackTraceFromOnErrorMessage } from '../../to
import { computeRawError, isError } from './error'
import type { RawError } from './error.types'
import { ErrorHandling, ErrorSource, NonErrorPrefix } from './error.types'
import { getGlobalObject } from '../../tools/getGlobalObject'

export type UnhandledErrorCallback = (stackTrace: StackTrace, originalError?: any) => any

Expand Down Expand Up @@ -33,7 +34,13 @@ export function trackRuntimeError(errorObservable: Observable<RawError>) {
}

export function instrumentOnError(callback: UnhandledErrorCallback) {
return instrumentMethod(window, 'onerror', ({ parameters: [messageObj, url, line, column, errorObj] }) => {
const globalObject = getGlobalObject()

if (!('onerror' in globalObject)) {
return { stop: () => {} }
}

return instrumentMethod(globalObject, 'onerror', ({ parameters: [messageObj, url, line, column, errorObj] }) => {
let stackTrace
if (isError(errorObj)) {
stackTrace = computeStackTrace(errorObj)
Expand All @@ -45,7 +52,13 @@ export function instrumentOnError(callback: UnhandledErrorCallback) {
}

export function instrumentUnhandledRejection(callback: UnhandledErrorCallback) {
return instrumentMethod(window, 'onunhandledrejection', ({ parameters: [e] }) => {
const globalObject = getGlobalObject()

if (!('onunhandledrejection' in globalObject)) {
return { stop: () => {} }
}

return instrumentMethod(globalObject, 'onunhandledrejection', ({ parameters: [e] }) => {
const reason = e.reason || 'Empty reason'
const stack = computeStackTrace(reason)
callback(stack, reason)
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/domain/session/sessionConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export const SESSION_COOKIE_EXPIRATION_DELAY = ONE_YEAR
export const SessionPersistence = {
COOKIE: 'cookie',
LOCAL_STORAGE: 'local-storage',
SERVICE_WORKER: 'service-worker',
} as const
export type SessionPersistence = (typeof SessionPersistence)[keyof typeof SessionPersistence]
12 changes: 12 additions & 0 deletions packages/core/src/domain/session/sessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ export function stopSessionManager() {
}

function trackActivity(configuration: Configuration, expandOrRenewSession: () => void) {
if (typeof window === 'undefined') {
return
}

const { stop } = addEventListeners(
configuration,
window,
Expand All @@ -123,6 +127,10 @@ function trackActivity(configuration: Configuration, expandOrRenewSession: () =>
}

function trackVisibility(configuration: Configuration, expandSession: () => void) {
if (typeof document === 'undefined') {
return
}

const expandSessionWhenVisible = () => {
if (document.visibilityState === 'visible') {
expandSession()
Expand All @@ -139,6 +147,10 @@ function trackVisibility(configuration: Configuration, expandSession: () => void
}

function trackResume(configuration: Configuration, cb: () => void) {
if (typeof window === 'undefined') {
return
}

const { stop } = addEventListener(configuration, window, DOM_EVENT.RESUME, cb, { capture: true })
stopCallbacks.push(stop)
}
96 changes: 96 additions & 0 deletions packages/core/src/domain/session/sessionStore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ describe('session store', () => {
function disableCookies() {
spyOnProperty(document, 'cookie', 'get').and.returnValue('')
}

function disableLocalStorage() {
spyOn(Storage.prototype, 'getItem').and.throwError('unavailable')
}
Expand Down Expand Up @@ -609,4 +610,99 @@ describe('session store', () => {
expect(otherUpdateSpy).toHaveBeenCalled()
})
})

describe('session store with service worker persistence', () => {
let sessionStoreManager: SessionStore
let fakeCacheStore: Record<string, string>
let originalCaches: any

beforeEach(() => {
fakeCacheStore = {}
originalCaches = Object.getOwnPropertyDescriptor(window, 'caches')

const mockCaches = {
open: jasmine.createSpy('open').and.callFake(() => {
return Promise.resolve({
match: jasmine.createSpy('match').and.callFake((key: string) => {
if (fakeCacheStore[key]) {
return Promise.resolve(new Response(fakeCacheStore[key]))
}
return Promise.resolve(undefined)
}),
put: jasmine.createSpy('put').and.callFake((key: string, response: Response) => {
return response.text().then((text: string) => {
fakeCacheStore[key] = text
})
}),
})
}),
}

Object.defineProperty(window, 'caches', {
get: () => mockCaches,
configurable: true,
})
})

afterEach(() => {
if (originalCaches) {
Object.defineProperty(window, 'caches', originalCaches)
} else {
delete (window as any).caches
}
sessionStoreManager?.stop()
})

it('should select service worker strategy type', () => {
const strategyType = selectSessionStoreStrategyType({
...DEFAULT_INIT_CONFIGURATION,
sessionPersistence: SessionPersistence.SERVICE_WORKER,
})
expect(strategyType).toEqual(jasmine.objectContaining({ type: SessionPersistence.SERVICE_WORKER }))
})

it('should initialize session store using service worker strategy', async () => {
const strategyType = selectSessionStoreStrategyType({
...DEFAULT_INIT_CONFIGURATION,
sessionPersistence: SessionPersistence.SERVICE_WORKER,
})
expect(strategyType).toBeDefined()

sessionStoreManager = startSessionStore(strategyType!, DEFAULT_CONFIGURATION, PRODUCT_KEY, () => ({
isTracked: true,
trackingType: FakeTrackingType.TRACKED,
}))
await new Promise((resolve) => setTimeout(resolve, 20))

const session = sessionStoreManager.getSession()
expect(session.isExpired).toEqual(IS_EXPIRED)
expect(session.anonymousId).toEqual(jasmine.any(String))
})

it('should update session state using service worker strategy', async () => {
const strategyType = selectSessionStoreStrategyType({
...DEFAULT_INIT_CONFIGURATION,
sessionPersistence: SessionPersistence.SERVICE_WORKER,
});
sessionStoreManager = startSessionStore(
strategyType!,
DEFAULT_CONFIGURATION,
PRODUCT_KEY,
() => ({
isTracked: true,
trackingType: FakeTrackingType.TRACKED,
})
);
await new Promise(resolve => setTimeout(resolve, 20));

sessionStoreManager.expandOrRenewSession();
await new Promise(resolve => setTimeout(resolve, 20));

sessionStoreManager.updateSessionState({ extra: 'value' });
await new Promise(resolve => setTimeout(resolve, 20));

const session = sessionStoreManager.getSession();
expect(session.extra).toEqual('value');
});
})
})
20 changes: 16 additions & 4 deletions packages/core/src/domain/session/sessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import type { SessionState } from './sessionState'
import { initLocalStorageStrategy, selectLocalStorageStrategy } from './storeStrategies/sessionInLocalStorage'
import { processSessionStoreOperations } from './sessionStoreOperations'
import { SessionPersistence } from './sessionConstants'
import { initServiceWorkerStrategy, selectServiceWorkerStrategy } from './storeStrategies/sessionInServiceWorker'
import type { CookieOptions } from '../../browser/cookie'
import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy'

export interface SessionStore {
expandOrRenewSession: () => void
Expand Down Expand Up @@ -52,6 +55,9 @@ export function selectSessionStoreStrategyType(
case SessionPersistence.LOCAL_STORAGE:
return selectLocalStorageStrategy()

case SessionPersistence.SERVICE_WORKER:
return selectServiceWorkerStrategy()

case undefined: {
let sessionStoreStrategyType = selectCookieStrategy(initConfiguration)
if (!sessionStoreStrategyType && initConfiguration.allowFallbackToLocalStorage) {
Expand Down Expand Up @@ -81,10 +87,16 @@ export function startSessionStore<TrackingType extends string>(
const expireObservable = new Observable<void>()
const sessionStateUpdateObservable = new Observable<{ previousState: SessionState; newState: SessionState }>()

const sessionStoreStrategy =
sessionStoreStrategyType.type === SessionPersistence.COOKIE
? initCookieStrategy(configuration, sessionStoreStrategyType.cookieOptions)
: initLocalStorageStrategy(configuration)
const strategyInitializers: Record<SessionPersistence, (configuration: Configuration, options?: CookieOptions) => SessionStoreStrategy> = {
[SessionPersistence.COOKIE]: (configuration, cookieOptions) => initCookieStrategy(configuration, cookieOptions!),
[SessionPersistence.LOCAL_STORAGE]: (configuration) => initLocalStorageStrategy(configuration),
[SessionPersistence.SERVICE_WORKER]: (configuration) => initServiceWorkerStrategy(configuration),
}

const sessionStoreStrategy = strategyInitializers[sessionStoreStrategyType.type](
configuration,
'cookieOptions' in sessionStoreStrategyType ? sessionStoreStrategyType.cookieOptions : undefined
)
const { expireSession } = sessionStoreStrategy

const watchSessionTimeoutId = setInterval(watchSession, STORAGE_POLL_DELAY)
Expand Down
Loading