diff --git a/src/__tests__/extensions/replay/sessionrecording.test.ts b/src/__tests__/extensions/replay/sessionrecording.test.ts
index 8ba67e05b..f79796d29 100644
--- a/src/__tests__/extensions/replay/sessionrecording.test.ts
+++ b/src/__tests__/extensions/replay/sessionrecording.test.ts
@@ -536,7 +536,6 @@ describe('SessionRecording', () => {
$window_id: 'windowId',
},
{
- transport: 'XHR',
method: 'POST',
endpoint: '/s/',
_noTruncate: true,
@@ -575,7 +574,6 @@ describe('SessionRecording', () => {
},
{
method: 'POST',
- transport: 'XHR',
endpoint: '/s/',
_noTruncate: true,
_batchKey: 'recordings',
@@ -660,7 +658,6 @@ describe('SessionRecording', () => {
},
{
method: 'POST',
- transport: 'XHR',
endpoint: '/s/',
_noTruncate: true,
_batchKey: 'recordings',
@@ -1288,7 +1285,6 @@ describe('SessionRecording', () => {
_noTruncate: true,
endpoint: '/s/',
method: 'POST',
- transport: 'XHR',
}
)
expect(sessionRecording['buffer']).toEqual({
diff --git a/src/__tests__/retry-queue.test.ts b/src/__tests__/retry-queue.test.ts
index 83356ffb3..87e77254c 100644
--- a/src/__tests__/retry-queue.test.ts
+++ b/src/__tests__/retry-queue.test.ts
@@ -14,7 +14,7 @@ const defaultRequestOptions: CaptureOptions = {
}
describe('RetryQueue', () => {
- const onXHRError = jest.fn().mockImplementation(console.error)
+ const onRequestError = jest.fn().mockImplementation(console.error)
const rateLimiter = new RateLimiter()
let retryQueue: RetryQueue
@@ -26,7 +26,7 @@ describe('RetryQueue', () => {
})
beforeEach(() => {
- retryQueue = new RetryQueue(onXHRError, rateLimiter)
+ retryQueue = new RetryQueue(onRequestError, rateLimiter)
assignableWindow.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass)
assignableWindow.navigator.sendBeacon = jest.fn()
@@ -112,7 +112,7 @@ describe('RetryQueue', () => {
expect(retryQueue.queue.length).toEqual(0)
expect(assignableWindow.XMLHttpRequest).toHaveBeenCalledTimes(4)
- expect(onXHRError).toHaveBeenCalledTimes(0)
+ expect(onRequestError).toHaveBeenCalledTimes(0)
})
it('does not process event retry requests when events are rate limited', () => {
@@ -144,7 +144,7 @@ describe('RetryQueue', () => {
// clears queue
expect(retryQueue.queue.length).toEqual(0)
expect(assignableWindow.XMLHttpRequest).toHaveBeenCalledTimes(1)
- expect(onXHRError).toHaveBeenCalledTimes(0)
+ expect(onRequestError).toHaveBeenCalledTimes(0)
})
it('does not process recording retry requests when they are rate limited', () => {
@@ -176,7 +176,7 @@ describe('RetryQueue', () => {
// clears queue
expect(retryQueue.queue.length).toEqual(0)
expect(assignableWindow.XMLHttpRequest).toHaveBeenCalledTimes(2)
- expect(onXHRError).toHaveBeenCalledTimes(0)
+ expect(onRequestError).toHaveBeenCalledTimes(0)
})
it('tries to send requests via beacon on unload', () => {
@@ -201,12 +201,12 @@ describe('RetryQueue', () => {
expect(assignableWindow.navigator.sendBeacon).toHaveBeenCalledTimes(0)
})
- it('when you flush the queue onXHRError is passed to xhr', () => {
- const xhrSpy = jest.spyOn(SendRequest, 'xhr')
+ it('when you flush the queue onError is passed to xhr', () => {
+ const xhrSpy = jest.spyOn(SendRequest, 'request')
enqueueRequests()
retryQueue.flush()
fastForwardTimeAndRunTimer()
- expect(xhrSpy).toHaveBeenCalledWith(expect.objectContaining({ onXHRError: onXHRError }))
+ expect(xhrSpy).toHaveBeenCalledWith(expect.objectContaining({ onError: onRequestError }))
})
it('enqueues requests when offline and flushes immediately when online again', () => {
@@ -220,7 +220,7 @@ describe('RetryQueue', () => {
// requests aren't attempted when we're offline
expect(assignableWindow.XMLHttpRequest).toHaveBeenCalledTimes(0)
// doesn't log that it is offline from the retry queue
- expect(onXHRError).toHaveBeenCalledTimes(0)
+ expect(onRequestError).toHaveBeenCalledTimes(0)
// queue stays the same
expect(retryQueue.queue.length).toEqual(4)
diff --git a/src/__tests__/send-request.test.ts b/src/__tests__/send-request.test.ts
index 4435e7714..176fcf3df 100644
--- a/src/__tests__/send-request.test.ts
+++ b/src/__tests__/send-request.test.ts
@@ -1,20 +1,29 @@
+/* eslint-disable compat/compat */
///
-import { addParamsToURL, encodePostData, xhr } from '../send-request'
+import { addParamsToURL, encodePostData, request } from '../send-request'
import { assert, boolean, property, uint8Array, VerbosityLevel } from 'fast-check'
-import { Compression, PostData, XHROptions, XHRParams } from '../types'
+import { Compression, PostData, XHROptions, RequestData, MinimalHTTPResponse } from '../types'
import { _isUndefined } from '../utils/type-utils'
+import { assignableWindow } from '../utils/globals'
+
+jest.mock('../utils/request-utils', () => ({
+ ...jest.requireActual('../utils/request-utils'),
+ SUPPORTS_XHR: true,
+ SUPPORTS_FETCH: true,
+ SUPPORTS_REQUEST: true,
+}))
jest.mock('../config', () => ({ DEBUG: false, LIB_VERSION: '1.23.45' }))
+const flushPromises = () => new Promise((r) => setTimeout(r, 0))
+
describe('send-request', () => {
describe('xhr', () => {
let mockXHR: XMLHttpRequest
- let xhrParams: (overrides?: Partial) => XHRParams
- let onXHRError: XHRParams['onXHRError']
- let checkForLimiting: XHRParams['onResponse']
- let xhrOptions: XHRParams['options']
+ let createRequestData: (overrides?: Partial) => RequestData
+ let checkForLimiting: RequestData['onResponse']
beforeEach(() => {
mockXHR = {
@@ -27,21 +36,21 @@ describe('send-request', () => {
status: 502,
} as Partial as XMLHttpRequest
- onXHRError = jest.fn()
checkForLimiting = jest.fn()
- xhrOptions = {}
- xhrParams = (overrides?: Partial) => {
+ createRequestData = (overrides?: Partial) => {
return {
url: 'https://any.posthog-instance.com?ver=1.23.45',
data: {},
headers: {},
- options: xhrOptions,
callback: () => {},
+ options: {
+ transport: 'XHR',
+ ...(overrides?.options || {}),
+ },
retriesPerformedSoFar: undefined,
retryQueue: {
enqueue: () => {},
- } as Partial as XHRParams['retryQueue'],
- onXHRError,
+ } as Partial as RequestData['retryQueue'],
onResponse: checkForLimiting,
...overrides,
}
@@ -50,56 +59,75 @@ describe('send-request', () => {
// ignore TS complaining about us cramming a fake in here
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- window.XMLHttpRequest = jest.fn(() => mockXHR) as unknown as XMLHttpRequest
+ assignableWindow.XMLHttpRequest = jest.fn(() => mockXHR) as unknown as XMLHttpRequest
})
- test('it adds the retry count to the URL', () => {
- const retryCount = Math.floor(Math.random() * 100) + 1 // make sure it is never 0
- xhr(
- xhrParams({
- retriesPerformedSoFar: retryCount,
+ test('performs the request with default params', () => {
+ request(
+ createRequestData({
+ retriesPerformedSoFar: 0,
url: 'https://any.posthog-instance.com/?ver=1.23.45&ip=7&_=1698404857278',
})
)
expect(mockXHR.open).toHaveBeenCalledWith(
'GET',
- `https://any.posthog-instance.com/?ver=1.23.45&ip=7&_=1698404857278&retry_count=${retryCount}`,
+ 'https://any.posthog-instance.com/?ver=1.23.45&ip=7&_=1698404857278',
true
)
})
- test('does not add retry count when it is 0', () => {
- xhr(
- xhrParams({
- retriesPerformedSoFar: 0,
+ test('it adds the retry count to the URL', () => {
+ const retryCount = Math.floor(Math.random() * 100) + 1 // make sure it is never 0
+ request(
+ createRequestData({
+ retriesPerformedSoFar: retryCount,
url: 'https://any.posthog-instance.com/?ver=1.23.45&ip=7&_=1698404857278',
})
)
expect(mockXHR.open).toHaveBeenCalledWith(
'GET',
- 'https://any.posthog-instance.com/?ver=1.23.45&ip=7&_=1698404857278',
+ `https://any.posthog-instance.com/?ver=1.23.45&ip=7&_=1698404857278&retry_count=${retryCount}`,
true
)
})
- describe('when xhr requests fail', () => {
- it('does not error if the configured onXHRError is not a function', () => {
- onXHRError = 'not a function' as unknown as XHRParams['onXHRError']
+ it('calls the on response handler', async () => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ // noinspection JSConstantReassignment
+ mockXHR.status = 200
+ request(createRequestData())
+ mockXHR.onreadystatechange?.({} as Event)
+ expect(checkForLimiting).toHaveBeenCalledWith({
+ statusCode: mockXHR.status,
+ responseText: mockXHR.responseText,
+ })
+ })
+
+ describe('when the requests fail', () => {
+ it('does not error if the configured onError is not a function', () => {
expect(() => {
- xhr(xhrParams())
+ request(
+ createRequestData({
+ onError: 'not a function' as unknown as RequestData['onError'],
+ })
+ )
mockXHR.onreadystatechange?.({} as Event)
}).not.toThrow()
})
it('calls the injected XHR error handler', () => {
- //cannot use an auto-mock from jest as the code checks if onXHRError is a Function
- let requestFromError
- onXHRError = (req) => {
- requestFromError = req
- }
- xhr(xhrParams())
+ //cannot use an auto-mock from jest as the code checks if onError is a Function
+ let requestFromError: MinimalHTTPResponse | undefined
+ request(
+ createRequestData({
+ onError: (req) => {
+ requestFromError = req
+ },
+ })
+ )
mockXHR.onreadystatechange?.({} as Event)
- expect(requestFromError).toHaveProperty('status', 502)
+ expect(requestFromError).toHaveProperty('statusCode', 502)
})
it('calls the on response handler - regardless of status', () => {
@@ -109,9 +137,141 @@ describe('send-request', () => {
// @ts-ignore
// noinspection JSConstantReassignment
mockXHR.status = Math.floor(Math.random() * 100)
- xhr(xhrParams())
+ request(createRequestData())
mockXHR.onreadystatechange?.({} as Event)
- expect(checkForLimiting).toHaveBeenCalledWith(mockXHR)
+ expect(checkForLimiting).toHaveBeenCalledWith({
+ statusCode: mockXHR.status,
+ responseText: mockXHR.responseText,
+ })
+ })
+ })
+ })
+
+ describe('fetch', () => {
+ let createRequestData: (overrides?: Partial) => RequestData
+ let checkForLimiting: RequestData['onResponse']
+
+ beforeEach(() => {
+ checkForLimiting = jest.fn()
+ createRequestData = (overrides?: Partial) => {
+ return {
+ url: 'https://any.posthog-instance.com?ver=1.23.45',
+ data: {},
+ headers: {},
+ callback: () => {},
+ retriesPerformedSoFar: undefined,
+ options: {
+ transport: 'fetch',
+ ...(overrides?.options || {}),
+ },
+
+ retryQueue: {
+ enqueue: () => {},
+ } as Partial as RequestData['retryQueue'],
+ onResponse: checkForLimiting,
+ ...overrides,
+ }
+ }
+
+ assignableWindow.fetch = jest.fn(() => {
+ return Promise.resolve({
+ status: 200,
+ json: () => Promise.resolve({}),
+ text: () => Promise.resolve(''),
+ }) as any
+ })
+ })
+
+ test('it performs the request with default params', () => {
+ request(
+ createRequestData({
+ retriesPerformedSoFar: 0,
+ url: 'https://any.posthog-instance.com/?ver=1.23.45&ip=7&_=1698404857278',
+ })
+ )
+
+ expect(assignableWindow.fetch).toHaveBeenCalledWith(
+ `https://any.posthog-instance.com/?ver=1.23.45&ip=7&_=1698404857278`,
+ {
+ body: null,
+ headers: new Headers(),
+ keepalive: false,
+ method: 'GET',
+ }
+ )
+ })
+
+ test('it adds the retry count to the URL', () => {
+ const retryCount = Math.floor(Math.random() * 100) + 1 // make sure it is never 0
+ request(
+ createRequestData({
+ retriesPerformedSoFar: retryCount,
+ url: 'https://any.posthog-instance.com/?ver=1.23.45&ip=7&_=1698404857278',
+ })
+ )
+ expect(assignableWindow.fetch).toHaveBeenCalledWith(
+ `https://any.posthog-instance.com/?ver=1.23.45&ip=7&_=1698404857278&retry_count=${retryCount}`,
+ {
+ body: null,
+ headers: new Headers(),
+ keepalive: false,
+ method: 'GET',
+ }
+ )
+ })
+
+ it('calls the on response handler', async () => {
+ request(createRequestData())
+ await flushPromises()
+ expect(checkForLimiting).toHaveBeenCalledWith({
+ statusCode: 200,
+ responseText: '',
+ })
+ })
+
+ describe('when the requests fail', () => {
+ beforeEach(() => {
+ assignableWindow.fetch = jest.fn(
+ () =>
+ Promise.resolve({
+ status: 502,
+ text: () => Promise.resolve('oh no!'),
+ }) as any
+ )
+ })
+
+ it('does not error if the configured onError is not a function', () => {
+ expect(() => {
+ request(
+ createRequestData({
+ onError: 'not a function' as unknown as RequestData['onError'],
+ })
+ )
+ }).not.toThrow()
+ })
+
+ it('calls the injected XHR error handler', async () => {
+ //cannot use an auto-mock from jest as the code checks if onError is a Function
+ let requestFromError: MinimalHTTPResponse | undefined
+ request(
+ createRequestData({
+ onError: (req) => {
+ requestFromError = req
+ },
+ })
+ )
+ await flushPromises()
+
+ expect(requestFromError).toHaveProperty('statusCode', 502)
+ })
+
+ it('calls the on response handler - regardless of status', async () => {
+ request(createRequestData())
+ await flushPromises()
+ expect(checkForLimiting).toHaveBeenCalledWith({
+ statusCode: 502,
+ responseText: 'oh no!',
+ })
})
})
})
diff --git a/src/extensions/exception-autocapture/index.ts b/src/extensions/exception-autocapture/index.ts
index 3ae4e6d7a..17a97e6ce 100644
--- a/src/extensions/exception-autocapture/index.ts
+++ b/src/extensions/exception-autocapture/index.ts
@@ -140,7 +140,6 @@ export class ExceptionObserver {
*/
sendExceptionEvent(properties: { [key: string]: any }) {
this.instance.capture('$exception', properties, {
- transport: 'XHR',
method: 'POST',
endpoint: EXCEPTION_INGESTION_ENDPOINT,
_noTruncate: true,
diff --git a/src/extensions/replay/sessionrecording.ts b/src/extensions/replay/sessionrecording.ts
index 4dda8e582..e6be439c2 100644
--- a/src/extensions/replay/sessionrecording.ts
+++ b/src/extensions/replay/sessionrecording.ts
@@ -834,7 +834,6 @@ export class SessionRecording {
private _captureSnapshot(properties: Properties) {
// :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
this.instance.capture('$snapshot', properties, {
- transport: 'XHR',
method: 'POST',
endpoint: this._endpoint,
_noTruncate: true,
diff --git a/src/posthog-core.ts b/src/posthog-core.ts
index 8388c799e..f4871df71 100644
--- a/src/posthog-core.ts
+++ b/src/posthog-core.ts
@@ -21,7 +21,7 @@ import { clearOptInOut, hasOptedIn, hasOptedOut, optIn, optOut, userOptedOut } f
import { cookieStore, localStore } from './storage'
import { RequestQueue } from './request-queue'
import { compressData, decideCompression } from './compression'
-import { addParamsToURL, encodePostData, xhr } from './send-request'
+import { addParamsToURL, encodePostData, request } from './send-request'
import { RetryQueue } from './retry-queue'
import { SessionIdManager } from './sessionid'
import {
@@ -58,6 +58,7 @@ import { logger } from './utils/logger'
import { document, userAgent } from './utils/globals'
import { SessionPropsManager } from './session-props'
import { _isBlockedUA } from './utils/blocked-uas'
+import { SUPPORTS_REQUEST } from './utils/request-utils'
/*
SIMPLE STYLE GUIDE:
@@ -92,12 +93,11 @@ const PRIMARY_INSTANCE_NAME = 'posthog'
*/
// http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/
// https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials
-const USE_XHR = window?.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()
// IE<10 does not support cross-origin XHR's but script tags
// with defer won't block window.onload; ENQUEUE_REQUESTS
// should only be true for Opera<12
-let ENQUEUE_REQUESTS = !USE_XHR && userAgent?.indexOf('MSIE') === -1 && userAgent?.indexOf('Mozilla') === -1
+let ENQUEUE_REQUESTS = !SUPPORTS_REQUEST && userAgent?.indexOf('MSIE') === -1 && userAgent?.indexOf('Mozilla') === -1
export const defaultConfig = (): PostHogConfig => ({
api_host: 'https://app.posthog.com',
@@ -137,7 +137,7 @@ export const defaultConfig = (): PostHogConfig => ({
property_blacklist: [],
respect_dnt: false,
sanitize_properties: null,
- xhr_headers: {}, // { header: value, header2: value }
+ request_headers: {}, // { header: value, header2: value }
inapp_protocol: '//',
inapp_link_new_window: false,
request_batching: true,
@@ -149,8 +149,8 @@ export const defaultConfig = (): PostHogConfig => ({
advanced_disable_feature_flags: false,
advanced_disable_feature_flags_on_first_load: false,
advanced_disable_toolbar_metrics: false,
- on_xhr_error: (req) => {
- const error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText
+ on_request_error: (req) => {
+ const error = 'Bad HTTP status: ' + req.statusCode + ' ' + req.responseText
logger.error(error)
},
get_device_id: (uuid) => uuid,
@@ -425,6 +425,9 @@ export class PostHog {
}
}
+ // Check for deprecated params that might still be in use
+ config.request_headers = config.request_headers || config.xhr_headers
+
this.set_config(
_extend({}, defaultConfig(), config, {
name: name,
@@ -447,7 +450,7 @@ export class PostHog {
this.persistence = new PostHogPersistence(this.config)
this._requestQueue = new RequestQueue(this._handle_queued_event.bind(this))
- this._retryQueue = new RetryQueue(this.config.on_xhr_error, this.rateLimiter)
+ this._retryQueue = new RetryQueue(this.config.on_request_error, this.rateLimiter)
this.__captureHooks = []
this.__request_queue = []
@@ -625,23 +628,23 @@ export class PostHog {
return null
}
- if (USE_XHR) {
+ if (SUPPORTS_REQUEST) {
return function (response) {
callback(response, data)
}
- } else {
- // if the user gives us a callback, we store as a random
- // property on this instances jsc function and update our
- // callback string to reflect that.
- const jsc = this._jsc
- const randomized_cb = '' + Math.floor(Math.random() * 100000000)
- const callback_string = this.config.callback_fn + '[' + randomized_cb + ']'
- jsc[randomized_cb] = function (response: any) {
- delete jsc[randomized_cb]
- callback(response, data)
- }
- return callback_string
}
+
+ // if the user gives us a callback, we store as a random
+ // property on this instances jsc function and update our
+ // callback string to reflect that.
+ const jsc = this._jsc
+ const randomized_cb = '' + Math.floor(Math.random() * 100000000)
+ const callback_string = this.config.callback_fn + '[' + randomized_cb + ']'
+ jsc[randomized_cb] = function (response: any) {
+ delete jsc[randomized_cb]
+ callback(response, data)
+ }
+ return callback_string
}
_handle_unload(): void {
@@ -695,7 +698,7 @@ export class PostHog {
}
options = _extend(DEFAULT_OPTIONS, options || {})
- if (!USE_XHR) {
+ if (!SUPPORTS_REQUEST) {
options.method = 'GET'
}
@@ -713,17 +716,17 @@ export class PostHog {
// send beacon is a best-effort, fire-and-forget mechanism on page unload,
// we don't want to throw errors here
}
- } else if (USE_XHR || !document) {
+ } else if (SUPPORTS_REQUEST || !document) {
try {
- xhr({
- url: url,
- data: data,
- headers: this.config.xhr_headers,
- options: options,
+ request({
+ url,
+ data,
+ headers: this.config.request_headers,
+ options,
callback,
retriesPerformedSoFar: 0,
retryQueue: this._retryQueue,
- onXHRError: this.config.on_xhr_error,
+ onError: this.config.on_request_error,
onResponse: this.rateLimiter.checkForLimiting,
})
} catch (e) {
@@ -1686,7 +1689,7 @@ export class PostHog {
*
* // extra HTTP request headers to set for each API request, in
* // the format {'Header-Name': value}
- * xhr_headers: {}
+ * response_headers: {}
*
* // protocol for fetching in-app message resources, e.g.
* // 'https://' or 'http://'; defaults to '//' (which defers to the
diff --git a/src/rate-limiter.ts b/src/rate-limiter.ts
index 5eea45fcf..2a738147d 100644
--- a/src/rate-limiter.ts
+++ b/src/rate-limiter.ts
@@ -1,3 +1,4 @@
+import { MinimalHTTPResponse } from 'types'
import { logger } from './utils/logger'
const oneMinuteInMilliseconds = 60 * 1000
@@ -18,9 +19,9 @@ export class RateLimiter {
return new Date().getTime() < retryAfter
}
- public checkForLimiting = (xmlHttpRequest: XMLHttpRequest): void => {
+ public checkForLimiting = (httpResponse: MinimalHTTPResponse): void => {
try {
- const text = xmlHttpRequest.responseText
+ const text = httpResponse.responseText
if (!text || !text.length) {
return
}
diff --git a/src/retry-queue.ts b/src/retry-queue.ts
index bea0c7c69..302c556fd 100644
--- a/src/retry-queue.ts
+++ b/src/retry-queue.ts
@@ -1,6 +1,6 @@
import { RequestQueueScaffold } from './base-request-queue'
-import { encodePostData, xhr } from './send-request'
-import { QueuedRequestData, RetryQueueElement } from './types'
+import { encodePostData, request } from './send-request'
+import { PostHogConfig, QueuedRequestData, RetryQueueElement } from './types'
import { RateLimiter } from './rate-limiter'
import { _isUndefined } from './utils/type-utils'
@@ -32,15 +32,15 @@ export class RetryQueue extends RequestQueueScaffold {
queue: RetryQueueElement[]
isPolling: boolean
areWeOnline: boolean
- onXHRError: (failedRequest: XMLHttpRequest) => void
+ onRequestError: PostHogConfig['on_request_error']
rateLimiter: RateLimiter
- constructor(onXHRError: (failedRequest: XMLHttpRequest) => void, rateLimiter: RateLimiter) {
+ constructor(onRequestError: PostHogConfig['on_request_error'], rateLimiter: RateLimiter) {
super()
this.isPolling = false
this.queue = []
this.areWeOnline = true
- this.onXHRError = onXHRError
+ this.onRequestError = onRequestError
this.rateLimiter = rateLimiter
if (!_isUndefined(window) && 'onLine' in window.navigator) {
@@ -130,7 +130,7 @@ export class RetryQueue extends RequestQueueScaffold {
return
}
- xhr({
+ request({
url,
data: data || {},
options: options || {},
@@ -138,7 +138,7 @@ export class RetryQueue extends RequestQueueScaffold {
retriesPerformedSoFar: retriesPerformedSoFar || 0,
callback,
retryQueue: this,
- onXHRError: this.onXHRError,
+ onError: this.onRequestError,
onResponse: this.rateLimiter.checkForLimiting,
})
}
diff --git a/src/send-request.ts b/src/send-request.ts
index 08bc0dace..15508a9ea 100644
--- a/src/send-request.ts
+++ b/src/send-request.ts
@@ -1,10 +1,11 @@
import { _each } from './utils'
import Config from './config'
-import { PostData, XHROptions, XHRParams } from './types'
-import { _HTTPBuildQuery } from './utils/request-utils'
+import { PostData, XHROptions, RequestData, MinimalHTTPResponse } from './types'
+import { SUPPORTS_FETCH, _HTTPBuildQuery } from './utils/request-utils'
import { _isArray, _isFunction, _isNumber, _isUint8Array, _isUndefined } from './utils/type-utils'
import { logger } from './utils/logger'
+import { window } from './utils/globals'
export const addParamsToURL = (
url: string,
@@ -61,7 +62,81 @@ export const encodePostData = (data: PostData | Uint8Array, options: Partial {
+ // NOTE: Until we are confident with it, we only use fetch if explicitly told so
+ if (window && SUPPORTS_FETCH && params.options.transport === 'fetch') {
+ const body = encodePostData(params.data, params.options)
+
+ const headers = new Headers()
+ _each(headers, function (headerValue, headerName) {
+ headers.append(headerName, headerValue)
+ })
+
+ if (params.options.method === 'POST' && !params.options.blob) {
+ headers.append('Content-Type', 'application/x-www-form-urlencoded')
+ }
+
+ let url = params.url
+
+ if (_isNumber(params.retriesPerformedSoFar) && params.retriesPerformedSoFar > 0) {
+ url = addParamsToURL(url, { retry_count: params.retriesPerformedSoFar }, {})
+ }
+
+ window
+ .fetch(url, {
+ method: params.options?.method || 'GET',
+ headers,
+ keepalive: params.options.method === 'POST',
+ body,
+ })
+ .then((response) => {
+ const statusCode = response.status
+ // Report to the callback handlers
+ return response.text().then((responseText) => {
+ params.onResponse?.({
+ statusCode,
+ responseText,
+ })
+
+ if (statusCode === 200) {
+ try {
+ params.callback?.(JSON.parse(responseText))
+ } catch (e) {
+ logger.error(e)
+ }
+ return
+ }
+
+ if (_isFunction(params.onError)) {
+ params.onError({
+ statusCode,
+ responseText,
+ })
+ }
+
+ // don't retry errors between 400 and 500 inclusive
+ if (statusCode < 400 || statusCode > 500) {
+ params.retryQueue.enqueue({
+ ...params,
+ headers,
+ retriesPerformedSoFar: (params.retriesPerformedSoFar || 0) + 1,
+ })
+ }
+ params.callback?.({ status: 0 })
+ })
+ })
+ .catch((error) => {
+ logger.error(error)
+ params.callback?.({ status: 0 })
+ })
+
+ return
+ }
+
+ return xhr(params)
+}
+
+const xhr = ({
url,
data,
headers,
@@ -69,10 +144,10 @@ export const xhr = ({
callback,
retriesPerformedSoFar,
retryQueue,
- onXHRError,
+ onError,
timeout = 60000,
onResponse,
-}: XHRParams) => {
+}: RequestData) => {
if (_isNumber(retriesPerformedSoFar) && retriesPerformedSoFar > 0) {
url = addParamsToURL(url, { retry_count: retriesPerformedSoFar }, {})
}
@@ -97,7 +172,11 @@ export const xhr = ({
req.onreadystatechange = () => {
// XMLHttpRequest.DONE == 4, except in safari 4
if (req.readyState === 4) {
- onResponse?.(req)
+ const minimalResponseSummary: MinimalHTTPResponse = {
+ statusCode: req.status,
+ responseText: req.responseText,
+ }
+ onResponse?.(minimalResponseSummary)
if (req.status === 200) {
if (callback) {
let response
@@ -110,8 +189,8 @@ export const xhr = ({
callback(response)
}
} else {
- if (_isFunction(onXHRError)) {
- onXHRError(req)
+ if (_isFunction(onError)) {
+ onError(minimalResponseSummary)
}
// don't retry errors between 400 and 500 inclusive
diff --git a/src/types.ts b/src/types.ts
index 3c4189345..841e7798f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -58,7 +58,7 @@ export type UUIDVersion = 'og' | 'v7'
export interface PostHogConfig {
api_host: string
api_method: string
- api_transport: string
+ api_transport?: 'XHR' | 'fetch'
ui_host: string | null
token: string
autocapture: boolean | AutocaptureConfig
@@ -97,8 +97,12 @@ export interface PostHogConfig {
opt_in_site_apps: boolean
respect_dnt: boolean
property_blacklist: string[]
- xhr_headers: { [header_name: string]: string }
- on_xhr_error: (failedRequest: XMLHttpRequest) => void
+ request_headers: { [header_name: string]: string }
+ on_request_error: (error: MinimalHTTPResponse) => void
+ /** @deprecated - use `request_headers` instead */
+ xhr_headers?: { [header_name: string]: string }
+ /** @deprecated - use `on_request_error` instead */
+ on_xhr_error?: (failedRequest: XMLHttpRequest) => void
inapp_protocol: string
inapp_link_new_window: boolean
request_batching: boolean
@@ -181,7 +185,7 @@ export enum Compression {
}
export interface XHROptions {
- transport?: 'XHR' | 'sendBeacon'
+ transport?: 'XHR' | 'fetch' | 'sendBeacon'
method?: 'POST' | 'GET'
urlQueryArgs?: { compression: Compression }
verbose?: boolean
@@ -213,11 +217,17 @@ export interface QueuedRequestData {
retriesPerformedSoFar?: number
}
-export interface XHRParams extends QueuedRequestData {
+// Minimal class to allow interop between different request methods (xhr / fetch)
+export interface MinimalHTTPResponse {
+ statusCode: number
+ responseText: string
+}
+
+export interface RequestData extends QueuedRequestData {
retryQueue: RetryQueue
- onXHRError: (req: XMLHttpRequest) => void
timeout?: number
- onResponse?: (req: XMLHttpRequest) => void
+ onError?: (req: MinimalHTTPResponse) => void
+ onResponse?: (req: MinimalHTTPResponse) => void
}
export interface DecideResponse {
diff --git a/src/utils/request-utils.ts b/src/utils/request-utils.ts
index a097001d6..1acf562e2 100644
--- a/src/utils/request-utils.ts
+++ b/src/utils/request-utils.ts
@@ -1,11 +1,15 @@
import { _each, _isValidRegex } from './'
-import { _isArray, _isUndefined } from './type-utils'
+import { _isArray, _isFunction, _isUndefined } from './type-utils'
import { logger } from './logger'
-import { document } from './globals'
+import { document, window } from './globals'
const localDomains = ['localhost', '127.0.0.1']
+export const SUPPORTS_XHR = !!(window?.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest())
+export const SUPPORTS_FETCH = !!(window?.fetch && _isFunction(window?.fetch))
+export const SUPPORTS_REQUEST = SUPPORTS_XHR || SUPPORTS_FETCH
+
/**
* IE11 doesn't support `new URL`
* so we can create an anchor element and use that to parse the URL