Skip to content

Commit 5caa208

Browse files
authored
Merge pull request #54 from rehanvdm/release/v1.2.2
feat: release v1.2.2
2 parents b1e8c49 + 250a3dd commit 5caa208

File tree

11 files changed

+190
-18
lines changed

11 files changed

+190
-18
lines changed

docs/API.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,7 @@ const observability: Observability = { ... }
673673
| --- | --- | --- |
674674
| <code><a href="#serverless-website-analytics.Observability.property.alarms">alarms</a></code> | <code><a href="#serverless-website-analytics.AlarmProps">AlarmProps</a></code> | Adds CloudWatch Alarms to the resources created by this construct. |
675675
| <code><a href="#serverless-website-analytics.Observability.property.dashboard">dashboard</a></code> | <code>boolean</code> | Adds a CloudWatch dashboard with metrics for the resources created by this construct. |
676+
| <code><a href="#serverless-website-analytics.Observability.property.loglevel">loglevel</a></code> | <code>string</code> | Sets the log level, defaults to `AUDIT`. |
676677

677678
---
678679

@@ -700,6 +701,20 @@ Adds a CloudWatch dashboard with metrics for the resources created by this const
700701

701702
---
702703

704+
##### `loglevel`<sup>Optional</sup> <a name="loglevel" id="serverless-website-analytics.Observability.property.loglevel"></a>
705+
706+
```typescript
707+
public readonly loglevel: string;
708+
```
709+
710+
- *Type:* string
711+
712+
Sets the log level, defaults to `AUDIT`.
713+
714+
Available options: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `AUDIT`.
715+
716+
---
717+
703718
### RateLimitProps <a name="RateLimitProps" id="serverless-website-analytics.RateLimitProps"></a>
704719

705720
#### Initializer <a name="Initializer" id="serverless-website-analytics.RateLimitProps.Initializer"></a>

src/backend.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function backend(
2727
VERSION: '0.0.0',
2828
AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
2929
NODE_OPTIONS: '--enable-source-maps',
30-
LOG_LEVEL: 'DEBUG', // isProd ? "AUDIT" : "DEBUG"
30+
LOG_LEVEL: props.observability!.loglevel!,
3131
};
3232
let defaultNodeJsFuncOpt = {};
3333

@@ -301,7 +301,7 @@ export function backend(
301301
hardError: true,
302302
softErrorFilter: logs.FilterPattern.all(
303303
logs.FilterPattern.stringValue('$.level', '=', 'audit'),
304-
logs.FilterPattern.stringValue('$.success', '=', 'false')
304+
logs.FilterPattern.booleanValue('$.success', false)
305305
),
306306
},
307307
},
@@ -311,7 +311,7 @@ export function backend(
311311
hardError: true,
312312
softErrorFilter: logs.FilterPattern.all(
313313
logs.FilterPattern.stringValue('$.level', '=', 'audit'),
314-
logs.FilterPattern.stringValue('$.success', '=', 'false')
314+
logs.FilterPattern.booleanValue('$.success', false)
315315
),
316316
},
317317
},
@@ -321,7 +321,7 @@ export function backend(
321321
hardError: true,
322322
softErrorFilter: logs.FilterPattern.all(
323323
logs.FilterPattern.stringValue('$.level', '=', 'audit'),
324-
logs.FilterPattern.stringValue('$.success', '=', 'false')
324+
logs.FilterPattern.booleanValue('$.success', false)
325325
),
326326
},
327327
},

src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ export interface Observability {
146146
* Adds a CloudWatch dashboard with metrics for the resources created by this construct.
147147
*/
148148
readonly dashboard?: boolean;
149+
150+
/**
151+
* Sets the log level, defaults to `AUDIT`. Available options: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `AUDIT`.
152+
*/
153+
readonly loglevel?: string;
149154
}
150155

151156
export interface RateLimitProps {
@@ -258,6 +263,17 @@ export class Swa extends Construct {
258263
}
259264
}
260265

266+
/* Set default log level if it is not defined */
267+
if (!props?.observability?.loglevel) {
268+
props = {
269+
...props,
270+
observability: {
271+
...props.observability,
272+
loglevel: 'AUDIT',
273+
},
274+
};
275+
}
276+
261277
const authProps = auth(scope, name, props);
262278
const backendAnalyticsProps = backendAnalytics(scope, name, props);
263279
const backendProps = backend(scope, name, props, authProps, backendAnalyticsProps);

src/src/backend/api-ingest/shared.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ export async function getInfoFromIpAndUa(ip: string, ua: string) {
2323
let cityName;
2424
let deviceType;
2525
let isBot;
26-
try {
27-
/* === IP === */
28-
if (ip) {
26+
27+
/* === IP === */
28+
if (ip) {
29+
try {
2930
const response = maxMindReader.city(ip);
3031
if (response.country) {
3132
countryIso = response.country.isoCode;
@@ -34,8 +35,13 @@ export async function getInfoFromIpAndUa(ip: string, ua: string) {
3435
cityName = response.city.names.en;
3536
}
3637
}
38+
} catch (err) {
39+
logger.warning('Parsing failed for IP', ip);
40+
logger.warning(err as Error);
3741
}
42+
}
3843

44+
try {
3945
/* === Device Type === */
4046
const uaParsed = new UAParser(ua);
4147
const device = uaParsed.getDevice();
@@ -46,9 +52,10 @@ export async function getInfoFromIpAndUa(ip: string, ua: string) {
4652
/* === Is bot === */
4753
isBot = isbot(ua);
4854
} catch (err) {
49-
logger.error('Parsing failed for IP/UA', ip, ua);
50-
logger.error(err as Error);
55+
logger.warning('Parsing failed for UA', ua);
56+
logger.warning(err as Error);
5157
}
58+
5259
return { countryIso, countryName, cityName, deviceType, isBot };
5360
}
5461

src/src/backend/lib/utils/lambda_logger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export class LambdaLog {
196196
if (message instanceof Error) {
197197
line = {
198198
date,
199-
level: LogLevel.ERROR,
199+
level: logLevel,
200200
env: this.env,
201201
msg: message.message,
202202
args: message.stack?.toString(),

src/src/frontend/src/components/TableData.vue

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref, computed } from "vue";
2+
import {ref, computed, reactive} from "vue";
33
import { humanizeNumber } from "@frontend/src/lib/ui_utils";
44
55
export type Column = {
@@ -8,6 +8,8 @@ export type Column = {
88
type: "string" | "number",
99
gridColumn: `${number}fr`,
1010
canFilter?: boolean,
11+
/* The column name to use for that row to open it as a URL */
12+
openExternalColumn?: string
1113
};
1214
export type Props = {
1315
columns: Column[];
@@ -87,6 +89,9 @@ const canPageForward = computed(() => {
8789
function rowClick(rowText: any) {
8890
emit('click', rowText)
8991
}
92+
93+
const hover = reactive<Record<any, boolean>>({});
94+
9095
</script>
9196

9297
<template>
@@ -97,12 +102,26 @@ function rowClick(rowText: any) {
97102
</div>
98103

99104
<div v-if="!loading">
100-
<div class="row" :style="rowGridColumnCss" v-for="row of pageData">
105+
<div class="row" :style="rowGridColumnCss" v-for="(row, rowIndex) in pageData">
101106
<template v-for="col of columns">
102107
<template v-if="col.type === 'string'">
103108
<el-tooltip :show-after="1000" :content="row[col.index]">
104-
<div v-if="col.canFilter" class="column column--overflow column--click" @click="rowClick(row[col.index])">
105-
{{ row[col.index] }}
109+
<div v-if="col.canFilter" class="column column--overflow">
110+
111+
<div style="display: flex;"
112+
@mouseover="hover[rowIndex] = true" @mouseleave="hover[rowIndex] = false">
113+
<!-- Column overflow is width 100% so will always push icon all the way to left, so the flex options won't work, which is fine for this -->
114+
<div class="column--overflow column--click" @click="rowClick(row[col.index])">
115+
{{ row[col.index] }}
116+
</div>
117+
<!-- && hover[rowIndex]-->
118+
<div v-if="col.openExternalColumn && hover[rowIndex]">
119+
<a :href="row[col.openExternalColumn]" target="_blank" rel="noopener noreferrer">
120+
<mdi-open-in-new style="padding: 2px 5px 0; font-size: 14px"></mdi-open-in-new>
121+
</a>
122+
</div>
123+
</div>
124+
106125
</div>
107126
<div v-lese class="column column--overflow">
108127
{{ row[col.index] }}

src/src/frontend/src/components/vue-material-design-icons.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import Cog from 'vue-material-design-icons/Cog.vue';
44
import InboxMultiple from 'vue-material-design-icons/InboxMultiple.vue';
55
import ChevronDown from 'vue-material-design-icons/ChevronDown.vue';
66
import Refresh from 'vue-material-design-icons/Refresh.vue';
7+
import OpenInNew from 'vue-material-design-icons/OpenInNew.vue';
78

89
export function registerIconComponents(app: App<Element>) {
910
app.component('mdi-cog', Cog);
1011
app.component('mdi-inbox-multiple', InboxMultiple);
1112
app.component('mdi-chevron-down', ChevronDown);
1213
app.component('mdi-refresh', Refresh);
14+
app.component('mdi-open-in-new', OpenInNew);
1315
}

src/src/frontend/src/views/page_stats/components/page_views.vue

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ watch(() => [loading.value], async () => {
2424
emit('loading', loading.value)
2525
})
2626
27-
let pageViews: Ref<PageView[] | undefined> = ref();
27+
type PageViewExternal = PageView & { external_url: string }
28+
29+
let pageViews: Ref<PageViewExternal[] | undefined> = ref();
2830
let pageViewsQueryExecutionId: string | undefined = undefined;
2931
let pageViewsNextToken: string | undefined = undefined;
3032
const isPageViewsSameSite = computed(() => !pageViews.value?.length ? true : uniqBy(pageViews.value, 'site').length === 1);
@@ -48,7 +50,15 @@ async function loadData()
4850
if(!pageViews.value)
4951
pageViews.value = [];
5052
51-
pageViews.value = pageViews.value.concat(resp.data);
53+
pageViews.value = pageViews.value.concat(
54+
resp.data.map((pv) => {
55+
const pageUrl = pv.page_url.startsWith("/") ? pv.page_url : `/${pv.page_url}`;
56+
return {
57+
...pv,
58+
external_url: `https://${pv.site}${pageUrl}`
59+
}
60+
})
61+
);
5262
// pageViews.value = pageViews.value.concat(resp.data.slice(0,20));
5363
pageViewsQueryExecutionId = resp.queryExecutionId;
5464
pageViewsNextToken = resp.nextToken;
@@ -6096,7 +6106,7 @@ onMounted(() => {
60966106
60976107
const columns: ComputedRef<Column[]> = computed(() => {
60986108
const ret: Column[] = [
6099-
{ name: "Page", type: "string", index: "page_url", gridColumn: "6fr", canFilter: true },
6109+
{ name: "Page", type: "string", index: "page_url", gridColumn: "6fr", canFilter: true, openExternalColumn: "external_url" },
61006110
{ name: "Views", type: "number", index: "views", gridColumn: "1fr" },
61016111
{ name: "Time on Page", type: "number", index: "avg_time_on_page", gridColumn: "2fr" },
61026112
];

src/src/frontend/src/views/page_stats/components/referrers.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ onMounted(() => {
6969
})
7070
7171
const columns: Column[] = [
72-
{ name: "Referrer", type: "string", index: "referrer", gridColumn: "6fr", canFilter: true },
72+
{ name: "Referrer", type: "string", index: "referrer", gridColumn: "6fr", canFilter: true, openExternalColumn: "referrer" },
7373
{ name: "Views", type: "number", index: "views", gridColumn: "2fr" },
7474
];
7575

src/src/tests/backend/api-ingest/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,4 +246,37 @@ describe('API Ingest', function () {
246246
const respData = JSON.parse(resp.body);
247247
expect(respData.message).to.equal('Site does not exist');
248248
});
249+
250+
it('View Page - can not lookup IP', async function () {
251+
this.timeout(TimeOut * 1000);
252+
253+
const context = apiGwContext();
254+
const pageView: V1PageViewInput = {
255+
site: 'simulated',
256+
user_id: 'test_user_id_1',
257+
session_id: 'test_session_id_1',
258+
page_id: 'test_page_id_1',
259+
page_url: '/test_page_id_1.html',
260+
page_opened_at: '2023-06-01T01:02:03Z',
261+
time_on_page: 0,
262+
// referrer: "",
263+
// referrer: 'something.com',
264+
// referrer: "tests.com/something",
265+
};
266+
const event: ApiGwEventOptions = {
267+
contentType: 'text/plain;charset=UTF-8',
268+
method: 'POST',
269+
path: '/v1/page/view',
270+
body: JSON.stringify(pageView),
271+
origin: 'localhost',
272+
// ip: '0.0.0.0',
273+
ip: 'x',
274+
ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6,2 Safari/605.1.15',
275+
};
276+
277+
setEnvVariables(TestConfig.env);
278+
const resp = await invokeLocalHandlerOrMakeAPICall(event, handler, TestConfig.apiIngestUrl, context);
279+
280+
expect(resp.statusCode).to.equal(200);
281+
});
249282
});

0 commit comments

Comments
 (0)