Skip to content

Commit e632987

Browse files
ccbblinblacha
andauthored
fix: analytics error due arcgis user agent parsing BM-1430 (#3562)
### Motivation Analytics lambda error due to ua parsing failure ### Modifications guard against no version ### Verification test --------- Co-authored-by: Blayne Chard <bchard@linz.govt.nz>
1 parent e3b48e8 commit e632987

File tree

7 files changed

+48
-12
lines changed

7 files changed

+48
-12
lines changed

packages/lambda-analytic-cloudfront/src/__test__/analytics.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ describe('analytic lambda', () => {
125125
const files = [...memory.files.keys()];
126126
assert.equal(files.length, 2); // two files one input one output
127127

128-
assert.ok(files[1].startsWith(`mem://cache/errors-${new Date().toISOString().slice(0, 12)}`));
128+
assert.ok(files[1].startsWith(`mem://cache/errors/${new Date().toISOString().slice(0, 12)}`));
129129

130130
const data = await fsa.read(new URL(files[1]));
131131
assert.ok(data.toString().includes(JSON.stringify('Hello')));

packages/lambda-analytic-cloudfront/src/handler.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { byDay, getOneHourAgo } from './date.js';
1010
import { Elastic } from './elastic.js';
1111
import { FileProcess, toFullDate } from './log.reader.js';
1212
import { LogStats } from './log.stats.js';
13+
import { ParserErrors } from './useragent/parser.js';
1314

1415
const gzipPromise = promisify(gzip);
1516

@@ -140,11 +141,18 @@ export async function main(req: LambdaRequest): Promise<void> {
140141

141142
// If anything fails to index write the errors out to a log file at the cache location
142143
if (Elastic.errors.length > 0) {
143-
const errorLocation = new URL(`./errors-${new Date().toISOString()}.json`, CacheLocation);
144+
const errorLocation = new URL(`./errors/${new Date().toISOString()}-elastic.json`, CacheLocation);
144145
req.log.fatal({ errorLocation: errorLocation.href }, 'log:index:failed');
145146
await fsa.write(errorLocation, JSON.stringify(Elastic.errors));
146147
}
147148

149+
// If any user agents cause a error when parsing, write them out
150+
if (ParserErrors.size > 0) {
151+
const errorLocation = new URL(`./errors/${new Date().toISOString()}-user-agent.json`, CacheLocation);
152+
req.log.warn({ errorLocation: errorLocation.href }, 'log:parse:failed');
153+
await fsa.write(errorLocation, JSON.stringify([...ParserErrors]));
154+
}
155+
148156
let failed = false;
149157
for (const ret of rets) {
150158
if (ret.status !== 'rejected') continue;

packages/lambda-analytic-cloudfront/src/useragent/__test__/parser.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import assert from 'node:assert';
22
import { describe, it } from 'node:test';
33

44
import { UaParser } from '../agent.js';
5+
import { ParserErrors } from '../parser.js';
56

67
describe('UserAgents', () => {
78
it('should parse common browsers', () => {
@@ -112,6 +113,19 @@ describe('UserAgents', () => {
112113
variant: 'pro',
113114
version: '2.8',
114115
});
116+
117+
assert.deepEqual(UaParser.parse('ArcGISRuntime-iOS'), {
118+
name: 'arcgis',
119+
variant: 'ios',
120+
os: 'unknown',
121+
version: 'unknown',
122+
});
123+
});
124+
125+
it('should return error on fatal errors', () => {
126+
const ret = UaParser.parse('ArcGISRuntime-/');
127+
assert.equal(ret?.name, 'error');
128+
assert.equal([...ParserErrors], 'ArcGISRuntime-/');
115129
});
116130

117131
it('should handle software', () => {

packages/lambda-analytic-cloudfront/src/useragent/agents/gis.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const ArcGis: Record<string, UserAgentParser> = {
1313
'ArcGISRuntime-': (ua: string) => {
1414
const chunks = ua.split('/');
1515
const variant = chunks[0].slice('ArcGISRuntime-'.length).toLowerCase();
16+
if (chunks.length === 1) return { name: 'arcgis', variant };
1617
const version = chunks[1].slice(0, chunks[1].indexOf('.'));
1718
const os = chunks[1].split(' ')[1].slice(1);
1819
if (isValidOs(os)) return { name: 'arcgis', variant, version, os };

packages/lambda-analytic-cloudfront/src/useragent/parser.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { UserAgentInfo, UserAgentOs, UserAgentParser } from './parser.types.js';
55
const OsMap: Record<string, UserAgentOs> = { ubuntu: 'linux' };
66
const UaParser: ParserConfig = { name: 'ua-parser-js', hit: 0 };
77
const Skipped: ParserConfig = { name: 'skipped', hit: 0 };
8+
const ParserError: ParserConfig = { name: 'error', hit: 0 };
9+
export const ParserErrors = new Set();
10+
811
interface ParserConfig {
912
name: string;
1013
hit: number;
@@ -34,16 +37,25 @@ export class UserAgentParsers {
3437
existing.parser.hit++;
3538
return existing.value;
3639
}
37-
const ret = this._parse(userAgent);
38-
if (ret.value) {
39-
if (ret.value.version == null) ret.value.version = 'unknown';
40-
if (ret.value.variant == null) ret.value.variant = 'unknown';
41-
if (ret.value.os == null) ret.value.os = 'unknown';
40+
41+
try {
42+
const ret = this._parse(userAgent);
43+
if (ret.value) {
44+
if (ret.value.version == null) ret.value.version = 'unknown';
45+
if (ret.value.variant == null) ret.value.variant = 'unknown';
46+
if (ret.value.os == null) ret.value.os = 'unknown';
47+
}
48+
ret.hit++;
49+
ret.parser.hit++;
50+
this.cache.set(userAgent, ret);
51+
return ret.value;
52+
} catch (e) {
53+
const val = { value: { name: 'error' }, parser: ParserError, hit: 0 };
54+
ParserErrors.add(userAgent);
55+
if (ParserErrors.size > 100) ParserErrors.clear(); // Only track last 100 Errors
56+
this.cache.set(userAgent, val);
57+
return val.value;
4258
}
43-
ret.hit++;
44-
ret.parser.hit++;
45-
this.cache.set(userAgent, ret);
46-
return ret.value;
4759
}
4860

4961
_parse(userAgent: string | undefined): ParserCache {

packages/lambda-analytics/src/__tests__/ua.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ describe('UserAgent', () => {
7676
),
7777
'arcgis-ios_100',
7878
);
79+
assert.equal(getUserAgent('ArcGISRuntime-iOS'), 'arcgis-ios_unknown');
7980
});
8081

8182
it('should parse arcgis pro', () => {

packages/lambda-analytics/src/ua.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function isGis(userAgent: string): string | void {
5252
if (userAgent.startsWith('ArcGISRuntime-')) {
5353
const chunks = userAgent.split('/');
5454
const variant = chunks[0].replace('ArcGISRuntime-', '').toLowerCase();
55-
const version = chunks[1].slice(0, chunks[1].indexOf('.'));
55+
const version = chunks[1]?.split('.')[0] || 'unknown';
5656
return 'arcgis-' + variant + '_' + version;
5757
}
5858

0 commit comments

Comments
 (0)