Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9ddbfbe
chore: Fix local jest tests not passing
ptang-nr Apr 17, 2026
6d31cdc
WIP - add caching and related tests
ptang-nr Apr 20, 2026
84fd2da
WIP - Update handling of feature session modes
ptang-nr Apr 21, 2026
b30a425
WIP - Reduce logging/harvesting test modes
ptang-nr Apr 21, 2026
88d40a1
WIP - Fix referencing undefined session
ptang-nr Apr 21, 2026
0e16832
WIP - update JSDocs
ptang-nr Apr 22, 2026
4b590cf
WIP - cleanup
ptang-nr Apr 22, 2026
9011b03
WIP - patch e2e test
ptang-nr Apr 22, 2026
c62667d
WIP - patch e2e test
ptang-nr Apr 22, 2026
6c62d8e
WIP - add explicit test for wiping cached RUM on session reset
ptang-nr Apr 22, 2026
50294ab
WIP - RUM response should always contain entitlement + sampling decis…
ptang-nr Apr 22, 2026
e85e090
fix: Add cachedRumResponse to session model object
ptang-nr Apr 23, 2026
b46c9c5
Merge branch 'main' into NR-509779-improve-startup
ptang-nr Apr 24, 2026
6339ac0
Merge branch 'main' into NR-509779-improve-startup
ptang-nr Apr 29, 2026
9d7d83f
Merge branch 'main' into NR-509779-improve-startup
ptang-nr May 6, 2026
2de2845
WIP - Use cached rum response when present
ptang-nr May 14, 2026
0025c40
Update src/features/logging/aggregate/index.js
ptang-nr May 14, 2026
9348e34
WIP - refactor logging mode + code review feedback
ptang-nr May 14, 2026
fbe5e9a
WIP - SR code review feedback
ptang-nr May 14, 2026
5c0fd51
WIP - ST code review feedback
ptang-nr May 14, 2026
b39279a
WIP - fix
ptang-nr May 19, 2026
cbc7e7a
WIP - refactor
ptang-nr May 19, 2026
3e7defc
Merge branch 'main' into NR-509779-improve-startup
ptang-nr Jun 3, 2026
6308019
fix: Setting shouldCacheResponse value
ptang-nr Jun 3, 2026
1e4c873
fix: Use cached RUM response without waiting for call
ptang-nr Jun 4, 2026
82b0f26
Merge branch 'main' into NR-509779-improve-startup
ptang-nr Jun 4, 2026
49da74d
fix: Bad merge
ptang-nr Jun 5, 2026
d774a7f
Merge branch 'main' into NR-509779-improve-startup
ptang-nr Jun 8, 2026
07d4cad
Merge branch 'main' into NR-509779-improve-startup
ptang-nr Jun 16, 2026
a58738a
Revert to default to 0 for falsy feature values
ptang-nr Jun 16, 2026
a8dbf3b
Merge branch 'main' into NR-509779-improve-startup
ptang-nr Jun 16, 2026
8bbda57
Troubleshoot flaky test
ptang-nr Jun 16, 2026
6076551
Add warning if invalid feature mode detected
ptang-nr Jun 16, 2026
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
204 changes: 3 additions & 201 deletions src/common/harvest/harvester.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../features/metrics/constants'
import { FEATURE_TO_ENDPOINT, JSERRORS, RUM, EVENTS, FEATURE_NAMES, BLOBS, LOGS } from '../../loaders/features/features'
Comment thread
ptang-nr marked this conversation as resolved.
import { VERSION } from '../constants/env'
import { globalScope, isWorkerScope } from '../constants/runtime'
import { FEATURE_TO_ENDPOINT, FEATURE_NAMES } from '../../loaders/features/features'
import { handle } from '../event-emitter/handle'
import { eventListenerOpts } from '../event-listener/event-listener-opts'
import { now } from '../timing/now'
import { subscribeToEOL } from '../unload/eol'
import { cleanURL } from '../url/clean-url'
import { obj, param } from '../url/encode'
import { warn } from '../util/console'
import { stringify } from '../util/stringify'
import { getSubmitMethod, xhr as xhrMethod, xhrFetch as fetchMethod } from '../util/submit-data'
import { dispatchGlobalEvent } from '../dispatch/global-event'
import { getSubmitMethod, xhr as xhrMethod } from '../util/submit-data'
import { send } from './send'

const RETRY = 'Harvester/Retry/'
const RETRY_ATTEMPTED = RETRY + 'Attempted/'
Expand Down Expand Up @@ -101,193 +93,3 @@ export class Harvester {
}
}
}

/**
* @typedef {import('./types.js').NetworkSendSpec} NetworkSendSpec
*/

const warnings = {}
/**
* Initiate a harvest call.
* @param {NetworkSendSpec} param0 Specification for sending data
* @returns {boolean} True if a network call was made. Note that this does not mean or guarantee that it was successful.
*/
export function send (agentRef, { endpoint, payload, localOpts = {}, submitMethod, cbFinished, raw, featureName, endpointVersion = 1 }) {
if (!agentRef.info.errorBeacon) return false

let { body, qs } = cleanPayload(payload)

if (Object.keys(body).length === 0 && !localOpts.sendEmptyBody) { // if there's no body to send, just run onfinish stuff and return
if (cbFinished) cbFinished({ sent: false })
return false
}

const protocol = agentRef.init.ssl === false ? 'http' : 'https'
const perceivedBeacon = agentRef.init.proxy.beacon || agentRef.info.errorBeacon
const url = raw
? `${protocol}://${perceivedBeacon}/${endpoint}`
: `${protocol}://${perceivedBeacon}${endpoint !== RUM ? '/' + endpoint : ''}/${endpointVersion}/${agentRef.info.licenseKey}`
const baseParams = !raw ? baseQueryString(agentRef, qs, endpoint) : ''
let payloadParams = obj(qs, agentRef.runtime.maxBytes)
if (baseParams === '' && payloadParams.startsWith('&')) {
payloadParams = payloadParams.substring(1)
}

const fullUrl = `${url}?${baseParams}${payloadParams}`
const gzip = !!qs?.attributes?.includes('gzip')

// all gzipped data is already in the correct format and needs no transformation
// all features going to 'events' endpoint should already be serialized & stringified
let stringBody = gzip || endpoint === EVENTS ? body : stringify(body)

// If body is null, undefined, or an empty object or array after stringifying, send an empty string instead.
if (!stringBody || stringBody.length === 0 || stringBody === '{}' || stringBody === '[]') stringBody = ''

// Warn--once per endpoint--if the agent tries to send large payloads
if (endpoint !== BLOBS && stringBody.length > 750000 && (warnings[endpoint] = (warnings[endpoint] || 0) + 1) === 1) warn(28, endpoint)

const headers = [{ key: 'content-type', value: 'text/plain' }]

/* Since workers don't support sendBeacon right now, they can only use XHR method.
Because they still do permit synch XHR, the idea is that at final harvest time (worker is closing),
we just make a BLOCKING request--trivial impact--with the remaining data as a temp fill-in for sendBeacon.
Following the removal of img-element method. */
let result = submitMethod({ url: fullUrl, body: stringBody, sync: localOpts.isFinalHarvest && isWorkerScope, headers })

if (!localOpts.isFinalHarvest && cbFinished) { // final harvests don't hold onto buffer data (shouldRetryOnFail is false), so cleanup isn't needed
if (submitMethod === xhrMethod) {
result.addEventListener('loadend', function () {
// `this` here in block refers to the XHR object in this scope, do not change the anon function to an arrow function
// status 0 refers to a local error, such as CORS or network failure, or a blocked request by the browser (e.g. adblocker)
const cbResult = { sent: this.status !== 0, status: this.status, retry: shouldRetry(this.status), fullUrl, xhr: this, responseText: this.responseText }
cbFinished(cbResult)

/** temporary audit of consistency of harvest metadata flags */
if (!shouldRetry(this.status)) trackHarvestMetadata()
}, eventListenerOpts(false))
} else if (submitMethod === fetchMethod) {
result.then(async function (response) {
const status = response.status
const cbResult = { sent: true, status, retry: shouldRetry(status), fullUrl, fetchResponse: response, responseText: await response.text() }
cbFinished(cbResult)
/** temporary audit of consistency of harvest metadata flags */
if (!shouldRetry(status)) trackHarvestMetadata()
})
}

function trackHarvestMetadata () {
try {
if (featureName === FEATURE_NAMES.jserrors && !body?.err) return

const hasReplay = baseParams.includes('hr=1')
const hasTrace = baseParams.includes('ht=1')
const hasError = qs?.attributes?.includes('hasError=true')

handle('harvest-metadata', [{
[featureName]: {
...(hasReplay && { hasReplay }),
...(hasTrace && { hasTrace }),
...(hasError && { hasError })
}
}], undefined, FEATURE_NAMES.metrics, agentRef.ee)
} catch (err) {
// do nothing
}
}
}

dispatchGlobalEvent({
drained: !!agentRef.runtime?.activatedFeatures,
type: 'data',
name: 'harvest',
feature: featureName,
data: {
endpoint,
headers,
payload,
submitMethod: getSubmitMethodName(),
raw,
synchronousXhr: !!(localOpts.isFinalHarvest && isWorkerScope)
}
})

return true

function shouldRetry (status) {
switch (status) {
case 408:
case 429:
case 500:
return true
}
return (status >= 502 && status <= 504) || (status >= 512 && status <= 530)
}

function getSubmitMethodName () {
if (submitMethod === xhrMethod) return 'xhr'
if (submitMethod === fetchMethod) return 'fetch'
return 'beacon'
}
}

/**
* Cleans and returns a payload object containing a body and qs
* object with key/value pairs. KV pairs where the value is null,
* undefined, or an empty string are removed to save on transmission
* size.
* @param {HarvestPayload} payload Payload to be sent to the endpoint.
* @returns {HarvestPayload} Cleaned payload payload to be sent to the endpoint.
*/
function cleanPayload (payload = {}) {
const clean = (input) => {
if ((typeof Uint8Array !== 'undefined' && input instanceof Uint8Array) || Array.isArray(input)) return input
if (typeof input === 'string') return input
return Object.entries(input || {}).reduce((accumulator, [key, value]) => {
if ((typeof value === 'number') ||
(typeof value === 'string' && value.length > 0) ||
(typeof value === 'object' && Object.keys(value || {}).length > 0)
) {
accumulator[key] = value
}
return accumulator
}, {})
}

return {
body: clean(payload.body),
qs: clean(payload.qs)
}
}

// The stuff that gets sent every time.
function baseQueryString (agentRef, qs, endpoint) {
const ref = agentRef.runtime.obfuscator.obfuscateString(cleanURL('' + globalScope.location))
const session = agentRef.runtime.session
const hr = !!session?.state.sessionReplaySentFirstChunk && session?.state.sessionReplayMode === 1 && endpoint !== JSERRORS
const ht = !!session?.state.traceHarvestStarted && session?.state.sessionTraceMode === 1 && ![LOGS, BLOBS].includes(endpoint)

const qps = [
'a=' + agentRef.info.applicationID,
param('sa', (agentRef.info.sa ? '' + agentRef.info.sa : '')),
param('v', VERSION),
transactionNameParam(),
param('ct', agentRef.runtime.customTransaction),
'&rst=' + now(),
'&ck=0', // ck param DEPRECATED - still expected by backend
'&s=' + (session?.state.value || '0'), // the 0 id encaps all untrackable and default traffic
param('ref', ref),
param('ptid', (agentRef.runtime.ptid ? '' + agentRef.runtime.ptid : ''))
]
if (hr) qps.push(param('hr', '1', qs))
if (ht) qps.push(param('ht', '1', qs))

return qps.join('')

// Constructs the transaction name param for the beacon URL.
// Prefers the obfuscated transaction name over the plain text.
// Falls back to making up a name.
function transactionNameParam () {
if (agentRef.info.transactionName) return param('to', agentRef.info.transactionName)
return param('t', agentRef.info.tNamePlain || 'Unnamed Transaction')
}
}
Loading
Loading