Skip to content

Commit

Permalink
1.1.0
Browse files Browse the repository at this point in the history
feat: analytics (#20)

fix: close panel on row unmount
  • Loading branch information
Artboomy authored Mar 8, 2024
1 parent 181ddcd commit 0adef73
Show file tree
Hide file tree
Showing 15 changed files with 254 additions and 14 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ node_modules/
*.zip
roadmap.md
yarn-error.log
src/secrets.json
2 changes: 1 addition & 1 deletion dist/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Net logs",
"version": "1.0.1",
"version": "1.1.0",
"manifest_version": 3,
"minimum_chrome_version": "88",
"description": "Extendable network logs debugger",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "netlogs",
"version": "1.0.1",
"version": "1.1.0",
"description": "Web extension for custom network logs representation",
"main": "index.js",
"author": "artboomy",
Expand Down Expand Up @@ -74,7 +74,7 @@
"prettier": "2.2.1",
"style-loader": "2.0.0",
"tsconfig-paths-webpack-plugin": "3.3.0",
"typescript": "^4.4.0",
"typescript": "^4.6.0",
"webpack": "^5.11.1",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.3.1",
Expand Down
138 changes: 138 additions & 0 deletions src/api/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import Error = chrome.cast.Error;

const GA_ENDPOINT = 'https://www.google-analytics.com/mp/collect';
const GA_DEBUG_ENDPOINT = 'https://www.google-analytics.com/debug/mp/collect';
import secrets from '../secrets.json';
// File taken from https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/tutorial.google-analytics/scripts/google-analytics.js
// Get via https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#recommended_parameters_for_reports
const MEASUREMENT_ID = secrets.measurement_id;
const API_SECRET = secrets.api_secret;
const DEFAULT_ENGAGEMENT_TIME_MSEC = 100;

// Duration of inactivity after which a new session is created
const SESSION_EXPIRATION_IN_MIN = 30;

type Params = {
session_id?: string;
engagement_time_msec?: number;
[key: string]: unknown;
};

class Analytics {
debug = false;

constructor(debug = false) {
this.debug = debug;
}

// Returns the client id, or creates a new one if one doesn't exist.
// Stores client id in local storage to keep the same client id as long as
// the extension is installed.
async getOrCreateClientId() {
let { clientId } = await chrome.storage.local.get('clientId');
if (!clientId) {
// Generate a unique client ID, the actual value is not relevant
clientId = self.crypto.randomUUID();
await chrome.storage.local.set({ clientId });
}
return clientId;
}

// Returns the current session id, or creates a new one if one doesn't exist or
// the previous one has expired.
async getOrCreateSessionId() {
// Use storage.session because it is only in memory
let { sessionData } = await chrome.storage.session.get('sessionData');
const currentTimeInMs = Date.now();
// Check if session exists and is still valid
if (sessionData && sessionData.timestamp) {
// Calculate how long ago the session was last updated
const durationInMin =
(currentTimeInMs - sessionData.timestamp) / 60000;
// Check if last update lays past the session expiration threshold
if (durationInMin > SESSION_EXPIRATION_IN_MIN) {
// Clear old session id to start a new session
sessionData = null;
} else {
// Update timestamp to keep session alive
sessionData.timestamp = currentTimeInMs;
await chrome.storage.session.set({ sessionData });
}
}
if (!sessionData) {
// Create and store a new session
sessionData = {
session_id: currentTimeInMs.toString(),
timestamp: currentTimeInMs.toString()
};
await chrome.storage.session.set({ sessionData });
}
return sessionData.session_id;
}

// Fires an event with optional params. Event names must only include letters and underscores.
async fireEvent(name: string, params: Params = {}) {
// Configure session id and engagement time if not present, for more details see:
// https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#recommended_parameters_for_reports
if (!params.session_id) {
params.session_id = await this.getOrCreateSessionId();
}
if (!params.engagement_time_msec) {
params.engagement_time_msec = DEFAULT_ENGAGEMENT_TIME_MSEC;
}

try {
const response = await fetch(
`${
this.debug ? GA_DEBUG_ENDPOINT : GA_ENDPOINT
}?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`,
{
method: 'POST',
body: JSON.stringify({
client_id: await this.getOrCreateClientId(),
events: [
{
name,
params
}
]
})
}
);
if (!this.debug) {
return;
}
console.log(await response.text());
} catch (e) {
console.error(
'Google Analytics request failed with an exception',
e
);
}
}

// Fire a page view event.
async firePageViewEvent(
pageTitle: string,
pageLocation: string,
additionalParams: Params = {}
) {
return this.fireEvent('page_view', {
page_title: pageTitle,
page_location: pageLocation,
...additionalParams
});
}

// Fire an error event.
async fireErrorEvent(error: Error, additionalParams: Params = {}) {
// Note: 'error' is a reserved event name and cannot be used
// see https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#reserved_names
return this.fireEvent('extension_error', {
...error,
...additionalParams
});
}
}

export default new Analytics(); //process.env.NODE_ENV === 'development');
2 changes: 1 addition & 1 deletion src/api/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class SandboxRuntime {
return {
manifest_version: 3,
name: 'Net logs',
version: '1.0.1'
version: '1.1.0'
};
}

Expand Down
10 changes: 10 additions & 0 deletions src/app/sandbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,47 @@ const P_CODE = 'KeyP';
// Since we are in iframe, F5 event for inspected page should be hoisted manually
document.addEventListener('keydown', (e) => {
const isModifierPressed = isMacOs() ? e.metaKey : e.ctrlKey;
let hotkeyType = '';
switch (e.code) {
case F5_CODE:
callParentVoid('devtools.inspectedWindow.reload');
hotkeyType = 'reload';
break;
case 'KeyR':
if (isModifierPressed) {
callParentVoid('devtools.inspectedWindow.reload');
hotkeyType = 'reload';
}
break;
case F_CODE:
if (isModifierPressed) {
window.postMessage({ type: 'focusSearch' }, '*');
hotkeyType = 'search';
}
break;
case U_CODE:
if (isModifierPressed && e.shiftKey) {
window.postMessage({ type: 'toggleHideUnrelated' }, '*');
hotkeyType = 'toggleHideUnrelated';
}
break;
case L_CODE:
if (isModifierPressed) {
window.postMessage({ type: 'clearList' }, '*');
hotkeyType = 'clearList';
}
break;
case P_CODE:
if (isModifierPressed) {
window.postMessage({ type: 'togglePreserveLog' }, '*');
hotkeyType = 'togglePreserveLog';
}
break;
default:
// console.log(e);
// pass
}
if (hotkeyType) {
callParentVoid('analytics.hotkey', hotkeyType);
}
});
2 changes: 2 additions & 0 deletions src/components/MimetypeSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useListStore } from '../controllers/network';
import { MultiSelect } from 'react-multi-select-component';
import isEqual from 'lodash.isequal';
import settings from '../controllers/settings';
import { callParentVoid } from '../utils';

const useStyles = createUseStyles({
root: {
Expand Down Expand Up @@ -51,6 +52,7 @@ export const MimetypeSelect: FC = memo(() => {
(mimeType) => !selectedMimeTypes.has(mimeType)
);
setHiddenMimeTypes(newHiddenMimeTypes);
callParentVoid('analytics.mimeFilterChange');
};
return (
<MultiSelect
Expand Down
7 changes: 6 additions & 1 deletion src/components/PanelMain.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import ErrorBoundary from './ErrorBoundary';
import { Header } from './Header';
import { createUseStyles } from 'react-jss';
Expand All @@ -15,6 +15,7 @@ import { FilterContext } from '../context/FilterContext';
import { useHotkey } from '../hooks/useHotkey';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.min.css';
import { callParentVoid } from '../utils';

const useStyles = createUseStyles({
'@global': {
Expand Down Expand Up @@ -74,6 +75,10 @@ export const PanelMain: React.FC = () => {
useHotkey('toggleHideUnrelated', () => setHideUnrelated(!hideUnrelated), [
hideUnrelated
]);

useEffect(() => {
callParentVoid('analytics.init');
}, []);
return (
<DndProvider backend={HTML5Backend}>
<SettingsContainer>
Expand Down
13 changes: 12 additions & 1 deletion src/components/PropTree.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { FC } from 'react';
import React, { FC, useEffect, useRef } from 'react';
import { createUseStyles } from 'react-jss';
import { Section, TSection } from './Section';
import { callParentVoid } from '../utils';

export type PropTreeProps = {
data: {
[key: string]: TSection;
Expand All @@ -16,6 +18,15 @@ const useStyles = createUseStyles({

export const PropTree: FC<PropTreeProps> = ({ data }) => {
const styles = useStyles();
const openedRef = useRef(Date.now());
useEffect(() => {
return () => {
callParentVoid(
'analytics.propTreeViewed',
String(Date.now() - openedRef.current)
);
};
}, []);
return (
<section className={styles.root}>
{Object.entries(data).map(([key, sectionData]) => (
Expand Down
18 changes: 17 additions & 1 deletion src/components/Row.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { memo, MouseEventHandler, useContext } from 'react';
import React, {
memo,
MouseEventHandler,
useContext,
useEffect,
useRef
} from 'react';
import ContentOnlyItem from '../models/ContentOnlyItem';
import { TransactionItemAbstract } from '../models/TransactionItem';
import { createUseStyles } from 'react-jss';
Expand Down Expand Up @@ -46,13 +52,23 @@ export const Row: React.FC<IRowProps> = memo(({ item, className }) => {
const { setValue } = useContext(ModalContext);
const tag = item.getTag();
const meta = item.getMeta();
const shouldClean = useRef(false);
const handleClick: MouseEventHandler = (e) => {
if (e.target === e.currentTarget) {
if (meta) {
setValue(<PropTree data={meta} />);
shouldClean.current = true;
}
}
};
useEffect(() => {
return () => {
if (shouldClean.current) {
setValue(null);
shouldClean.current = false;
}
};
}, []);
const commonProps = {
className,
date: (
Expand Down
5 changes: 5 additions & 0 deletions src/components/list/DropContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ContentOnlyItem from '../../models/ContentOnlyItem';
import { ItemType } from '../../models/types';
import TransactionItem from '../../models/TransactionItem';
import { toast } from 'react-toastify';
import { callParentVoid } from '../../utils';

const useStyles = createUseStyles({
dropZone: {
Expand Down Expand Up @@ -76,6 +77,10 @@ export const DropContainer: FC = ({ children }) => {
],
false
);
callParentVoid(
'analytics.fileOpen',
String(log.log.entries.length)
);
} catch (e) {
console.log('Error occurred:', e);
toast.error('Invalid har file');
Expand Down
Loading

0 comments on commit 0adef73

Please sign in to comment.