feat(client): report the browser and both versions in browser_info
Adds dc_version, adapter_version, browser, browser_version, platform and the raw user_agent to the browser_info row, so a support ticket can be read off the log and a hook can branch on the browser without parsing the user agent itself. The summary comes from a new parse-user-agent util, following the existing shared/utils convention. Also adds a Cypress spec asserting the table reaches the adapter interface, which is the only place the payload is observable end to end.
This commit is contained in:
1 parent
7abd260f98
commit
a770f3695c
3 files changed
+117
-2
No files matched your search
@@ -0,0 +1,57 @@
|
||||
// Marks this file as an ES module so its top-level consts don't collide with
|
||||
// other spec files under the TS type-checker.
|
||||
export {}
|
||||
//
|
||||
// The client sends a single-row `browser_info` input table with every service
|
||||
// call, describing where the request came from - the URL of the Data
|
||||
// Controller page itself, so an embedded report (and any parameters its author
|
||||
// added to the embed URL) can be told apart. Hook scripts read it from WORK.
|
||||
//
|
||||
// Asserted here on the adapter interface, which is the only place the payload
|
||||
// is observable end to end.
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
|
||||
context('browser_info input table: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(hostUrl, { timeout: 60000 })
|
||||
|
||||
// The evaluation agreement is per-origin localStorage state, so a fresh
|
||||
// profile (a new origin, or CI) lands on it before anything else.
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.find('#TCS input[type="checkbox"]').length) {
|
||||
cy.get('#TCS input[type="checkbox"]').check({ force: true })
|
||||
cy.wait(4000)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('1 | is sent with the service calls the app makes', () => {
|
||||
const payloads: string[] = []
|
||||
|
||||
cy.intercept('**/stp/execute/**', (req) => {
|
||||
if (typeof req.body === 'string') payloads.push(req.body)
|
||||
})
|
||||
|
||||
// Opening the editor directly makes the app call editors/getdata.
|
||||
cy.visit(`${hostUrl}/#/editor/DC_JSLIB.MPE_X_TEST`, { timeout: 60000 })
|
||||
cy.get('#hotTable', { timeout: 60000 }).should('exist')
|
||||
cy.wait(8000)
|
||||
|
||||
cy.then(() => {
|
||||
const joined = payloads.join('\n')
|
||||
|
||||
expect(
|
||||
payloads.length,
|
||||
`service calls captured (${payloads.length})`
|
||||
).to.be.greaterThan(0)
|
||||
expect(joined, 'browser_info table in the payload').to.contain(
|
||||
'browser_info'
|
||||
)
|
||||
// TIMEZONE is a column of browser_info and of no other input table, so
|
||||
// this shows the row itself arrived, not just the table name.
|
||||
expect(joined, 'browser_info row content in the payload').to.contain(
|
||||
'timezone'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -21,6 +21,7 @@ import { SasViyaService } from './sas-viya.service'
|
||||
import { ViyaApiFolder } from '../viya-api-explorer/models/viya-api-folder.model'
|
||||
import { ViyaApiFolderMembers } from '../viya-api-explorer/models/viya-api-folder-content.model'
|
||||
import { VaMessagingService } from './va-messaging.service'
|
||||
import { parseUserAgent } from '../shared/utils/parse-user-agent'
|
||||
import { VERSION } from '../../environments/version'
|
||||
|
||||
@Injectable({
|
||||
@@ -129,6 +130,7 @@ export class SasService {
|
||||
if (data && typeof data === 'object' && !data.browser_info) {
|
||||
const va = this.vaMessagingService.latestMessage()
|
||||
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
const ua = parseUserAgent(navigator.userAgent)
|
||||
data.browser_info = [
|
||||
{
|
||||
url: window.location.href,
|
||||
@@ -136,7 +138,12 @@ export class SasService {
|
||||
timezone: tz || '',
|
||||
tz_offset: new Date().getTimezoneOffset(),
|
||||
locale: navigator.language || '',
|
||||
version: VERSION.semverString,
|
||||
dc_version: VERSION.semverString,
|
||||
adapter_version: VERSION.adapterVersion,
|
||||
browser: ua.browser,
|
||||
browser_version: ua.browserVersion,
|
||||
platform: ua.platform,
|
||||
user_agent: navigator.userAgent || '',
|
||||
va_result_name: va?.resultName ?? '',
|
||||
va_row_count: va?.rowCount ?? 0,
|
||||
va_available_row_count: va?.availableRowCount ?? 0
|
||||
@@ -147,7 +154,8 @@ export class SasService {
|
||||
name: p.name,
|
||||
label: p.label ?? '',
|
||||
data_type: p.dataType ?? '',
|
||||
value: p.value === null || p.value === undefined ? '' : String(p.value)
|
||||
value:
|
||||
p.value === null || p.value === undefined ? '' : String(p.value)
|
||||
}))
|
||||
}
|
||||
if (va?.columns?.length) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Summarises `navigator.userAgent` into the three things worth logging about a
|
||||
* browser: its family, its version, and the platform it is running on.
|
||||
*
|
||||
* The raw user agent is kept alongside these - it is what a support ticket
|
||||
* actually needs - but a hook is far more likely to want to branch on a name
|
||||
* than to write its own user agent parsing.
|
||||
*
|
||||
* Order matters: Edge and Opera both claim to be Chrome, and Chrome claims to
|
||||
* be Safari, so the most specific token has to be tested first.
|
||||
*/
|
||||
export interface UserAgentSummary {
|
||||
browser: string
|
||||
browserVersion: string
|
||||
platform: string
|
||||
}
|
||||
|
||||
export function parseUserAgent(userAgent: string): UserAgentSummary {
|
||||
const ua = userAgent || ''
|
||||
const match = (re: RegExp) => (ua.match(re) || [])[1] || ''
|
||||
|
||||
let browser = 'Unknown'
|
||||
let browserVersion = ''
|
||||
|
||||
if (/Edg[A-Z]?\//.test(ua)) {
|
||||
browser = 'Edge'
|
||||
browserVersion = match(/Edg[A-Z]?\/([\d.]+)/)
|
||||
} else if (/OPR\//.test(ua)) {
|
||||
browser = 'Opera'
|
||||
browserVersion = match(/OPR\/([\d.]+)/)
|
||||
} else if (/Firefox\//.test(ua)) {
|
||||
browser = 'Firefox'
|
||||
browserVersion = match(/Firefox\/([\d.]+)/)
|
||||
} else if (/Chrome\//.test(ua)) {
|
||||
browser = 'Chrome'
|
||||
browserVersion = match(/Chrome\/([\d.]+)/)
|
||||
} else if (/Safari\//.test(ua)) {
|
||||
browser = 'Safari'
|
||||
browserVersion = match(/Version\/([\d.]+)/)
|
||||
}
|
||||
|
||||
let platform = 'Unknown'
|
||||
if (/Android/.test(ua)) platform = 'Android'
|
||||
else if (/iPhone|iPad|iPod/.test(ua)) platform = 'iOS'
|
||||
else if (/Windows/.test(ua)) platform = 'Windows'
|
||||
else if (/Mac OS X/.test(ua)) platform = 'macOS'
|
||||
else if (/Linux/.test(ua)) platform = 'Linux'
|
||||
|
||||
return { browser, browserVersion, platform }
|
||||
}
|
||||
Reference in new issue
Block a user