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

feat: send events when user leave the page #1146

Merged
merged 14 commits into from
Feb 10, 2022
Merged
2 changes: 1 addition & 1 deletion dev-utils/karma.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const { getWebpackConfig, BUNDLE_TYPES } = require('./build')
const polyfills = 'test/polyfills.+(js|ts)'

const specPattern =
'test/{*.spec.+(js|ts),!(e2e|integration|node|bundle|types)/*.spec.+(js|ts)}'
'test/{*.spec.+(js|ts),!(e2e|integration|node|bundle|types)/**/*.spec.+(js|ts)}'
const { tunnelIdentifier } = getSauceConnectOptions()

// makes all object properties configurable by default
Expand Down
19 changes: 12 additions & 7 deletions dev-utils/webdriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,21 @@ const logLevels = {
const debugMode = false
const debugLevel = logLevels.INFO.value

function isLogEntryATestFailure(entry, whitelist) {
function isLogEntryATestFailure(entry, whitelist = []) {
var result = false
if (logLevels[entry.level].value > logLevels.WARNING.value) {
result = true
if (whitelist) {
for (var i = 0, l = whitelist.length; i < l; i++) {
if (entry.message.indexOf(whitelist[i]) !== -1) {
result = false
break
}

// Chrome's versions lower than 81 had a bug where a preflight request with keepalive specified was not supported
// Bug info: https://bugs.chromium.org/p/chromium/issues/detail?id=835821
whitelist.push(
'Preflight request for request with keepalive specified is currently not supported'
)

for (var i = 0, l = whitelist.length; i < l; i++) {
if (entry.message.indexOf(whitelist[i]) !== -1) {
result = false
break
}
}
}
Expand Down
21 changes: 3 additions & 18 deletions packages/rum-core/src/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@

import { isPlatformSupported, isBrowser, now } from './common/utils'
import { patchAll } from './common/patching'
import { observePageVisibility } from './common/page-visibility'
import { state } from './state'

let enabled = false
export function bootstrap() {
export function bootstrap(configService, transactionService) {
if (isPlatformSupported()) {
patchAll()
bootstrapPerf()
observePageVisibility(configService, transactionService)
state.bootstrapTime = now()
enabled = true
} else if (isBrowser) {
Expand All @@ -44,19 +45,3 @@ export function bootstrap() {

return enabled
}

export function bootstrapPerf() {
if (document.visibilityState === 'hidden') {
state.lastHiddenStart = 0
}

window.addEventListener(
'visibilitychange',
() => {
if (document.visibilityState === 'hidden') {
state.lastHiddenStart = performance.now()
}
},
{ capture: true }
)
}
76 changes: 31 additions & 45 deletions packages/rum-core/src/common/apm-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@
import Queue from './queue'
import throttle from './throttle'
import NDJSON from './ndjson'
import { XHR_IGNORE } from './patching/patch-utils'
import { truncateModel, METADATA_MODEL } from './truncate'
import { ERRORS, TRANSACTIONS } from './constants'
import {
ERRORS,
HTTP_REQUEST_TIMEOUT,
QUEUE_FLUSH,
TRANSACTIONS
} from './constants'
import { noop } from './utils'
import { Promise } from './polyfills'
import {
Expand All @@ -38,6 +42,8 @@ import {
compressPayload
} from './compress'
import { __DEV__ } from '../state'
import { sendFetchRequest, shouldUseFetchWithKeepAlive } from './http/fetch'
import { sendXHR } from './http/xhr'

/**
* Throttling interval defaults to 60 seconds
Expand Down Expand Up @@ -75,6 +81,10 @@ class ApmServer {
() => this._loggingService.warn('Dropped events due to throttling!'),
{ limit, interval: THROTTLE_INTERVAL }
)

this._configService.observeEvent(QUEUE_FLUSH, () => {
this.queue.flush()
})
}

_postJson(endPoint, payload) {
Expand Down Expand Up @@ -124,53 +134,29 @@ class ApmServer {
_makeHttpRequest(
method,
url,
{ timeout = 10000, payload, headers, beforeSend } = {}
{ timeout = HTTP_REQUEST_TIMEOUT, payload, headers, beforeSend } = {}
) {
return new Promise(function (resolve, reject) {
var xhr = new window.XMLHttpRequest()
xhr[XHR_IGNORE] = true
xhr.open(method, url, true)
xhr.timeout = timeout

if (headers) {
for (var header in headers) {
if (headers.hasOwnProperty(header)) {
xhr.setRequestHeader(header, headers[header])
}
// This bring the possibility of sending requests that outlive the page.
if (shouldUseFetchWithKeepAlive(method, payload)) {
return sendFetchRequest(method, url, {
keepalive: true,
timeout,
payload,
headers
}).catch(reason => {
// Chrome, before the version 81 had a bug where a preflight request with keepalive specified was not supported
// xhr will be used as a fallback to cover fetch network errors, more info: https://fetch.spec.whatwg.org/#concept-network-error
// Bug info: https://bugs.chromium.org/p/chromium/issues/detail?id=835821
if (reason instanceof TypeError) {
return sendXHR(method, url, { timeout, payload, headers, beforeSend })
}
}

xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
const { status, responseText } = xhr
// An http 4xx or 5xx error. Signal an error.
if (status === 0 || (status > 399 && status < 600)) {
reject({ url, status, responseText })
} else {
resolve(xhr)
}
}
}

xhr.onerror = () => {
const { status, responseText } = xhr
reject({ url, status, responseText })
}
// bubble other kind of reasons to keep handling the failure
throw reason
})
}

let canSend = true
if (typeof beforeSend === 'function') {
canSend = beforeSend({ url, method, headers, payload, xhr })
}
if (canSend) {
xhr.send(payload)
} else {
reject({
url,
status: 0,
responseText: 'Request rejected by user configuration.'
})
}
})
return sendXHR(method, url, { timeout, payload, headers, beforeSend })
}

fetchConfig(serviceName, environment) {
Expand Down
8 changes: 8 additions & 0 deletions packages/rum-core/src/common/config-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,14 @@ class Config {
storage.setItem(LOCAL_CONFIG_KEY, JSON.stringify(config))
}
}

dispatchEvent(name, args) {
this.events.send(name, args)
}

observeEvent(name, fn) {
return this.events.observe(name, fn)
}
}

export default Config
14 changes: 13 additions & 1 deletion packages/rum-core/src/common/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ const TRANSACTION_END = 'transaction:end'
* Internal Events
*/
const CONFIG_CHANGE = 'config:change'
const QUEUE_FLUSH = 'queue:flush'
const QUEUE_ADD_TRANSACTION = 'queue:add_transaction'

/**
* Events types that are used to toggle auto instrumentations
Expand Down Expand Up @@ -147,6 +149,7 @@ const TRANSACTIONS = 'transactions'
*/
const CONFIG_SERVICE = 'ConfigService'
const LOGGING_SERVICE = 'LoggingService'
const TRANSACTION_SERVICE = 'TransactionService'
const APM_SERVER = 'ApmServer'

/**
Expand All @@ -164,6 +167,11 @@ const KEYWORD_LIMIT = 1024
*/
const SESSION_TIMEOUT = 30 * 60000

/**
* Default http request is set to 10 seconds (in milliseconds)
*/
const HTTP_REQUEST_TIMEOUT = 10000

export {
SCHEDULE,
INVOKE,
Expand All @@ -180,6 +188,8 @@ export {
TRANSACTION_START,
TRANSACTION_END,
CONFIG_CHANGE,
QUEUE_FLUSH,
QUEUE_ADD_TRANSACTION,
XMLHTTPREQUEST,
FETCH,
HISTORY,
Expand All @@ -204,11 +214,13 @@ export {
TRANSACTIONS,
CONFIG_SERVICE,
LOGGING_SERVICE,
TRANSACTION_SERVICE,
APM_SERVER,
TRUNCATED_TYPE,
FIRST_INPUT,
LAYOUT_SHIFT,
OUTCOME_SUCCESS,
OUTCOME_FAILURE,
SESSION_TIMEOUT
SESSION_TIMEOUT,
HTTP_REQUEST_TIMEOUT
}
96 changes: 96 additions & 0 deletions packages/rum-core/src/common/http/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* MIT License
*
* Copyright (c) 2017-present, Elasticsearch BV
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/

import { HTTP_REQUEST_TIMEOUT } from '../constants'
import { getNativeFetch } from '../patching/fetch-patch'
import { isResponseSuccessful } from './response-status'

// keepalive flag tends to limit the payload size to 64 KB
// although this size if set, will be up to the user agent
// in order to be conservative we set a limit a little lower than that
export const BYTE_LIMIT = 60000

export function shouldUseFetchWithKeepAlive(method, payload) {
const size = calculateSize(payload)
return shouldUseFetch() && method === 'POST' && size < BYTE_LIMIT
}

export function sendFetchRequest(
method,
url,
{ keepalive = false, timeout = HTTP_REQUEST_TIMEOUT, payload, headers }
) {
let timeoutConfig = {}
if (typeof AbortController === 'function') {
const controller = new AbortController()
timeoutConfig.signal = controller.signal
setTimeout(() => controller.abort(), timeout)
}

let fetchResponse
return getNativeFetch()(url, {
Copy link
Member

Choose a reason for hiding this comment

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

have to ignore this request as well, similar to XHR_IGNORE.

body: payload,
headers,
method,
keepalive, // used to allow the request to outlive the page.
credentials: 'omit',
...timeoutConfig
})
.then(response => {
fetchResponse = response
return fetchResponse.text()
})
.then(responseText => {
const bodyResponse = {
url,
status: fetchResponse.status,
responseText
}

if (!isResponseSuccessful(fetchResponse.status)) {
throw bodyResponse
}

return bodyResponse
})
}

export function shouldUseFetch() {
return typeof getNativeFetch() === 'function'
}

function calculateSize(payload) {
if (!payload) {
// IE 11 cannot create Blob from undefined
return 0
}

// If the payload is compressed it is going to be already a Blob
if (payload instanceof Blob) {
return payload.size
}

return new Blob([payload]).size
}
33 changes: 33 additions & 0 deletions packages/rum-core/src/common/http/response-status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* MIT License
*
* Copyright (c) 2017-present, Elasticsearch BV
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/

export function isResponseSuccessful(status) {
// An http 4xx or 5xx error. Signal an error.
if (status === 0 || (status > 399 && status < 600)) {
return false
}

return true
}
Loading