Skip to content

Commit 478279f

Browse files
committed
analytics
1 parent ed0b869 commit 478279f

File tree

9 files changed

+168
-10
lines changed

9 files changed

+168
-10
lines changed

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Google Analytics 4 Configuration
2+
# Copy this file to .env.local and fill in your values
3+
REACT_APP_GA_MEASUREMENT_ID=G-XXXXXXXXXX
4+
REACT_APP_GA_API_SECRET=your_api_secret_here

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sp-editor",
3-
"version": "7.11.2",
3+
"version": "7.12.0",
44
"private": true,
55
"homepage": ".",
66
"engines": {

public/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"name": "SP Editor",
33
"homepage_url": "https://microsoftedge.microsoft.com/addons/detail/affnnhcbfmcbbdlcadgkdbfafigmjdkk",
4-
"version": "7.11.2",
4+
"version": "7.12.0",
55
"description": "Create and update SharePoint Online/SP2013/SP2016/SP2019 css/js files, inject files to web, manage web/list properties, list Webhook",
66
"manifest_version": 3,
77
"devtools_page": "devtools.html",
8-
"permissions": ["activeTab", "scripting", "downloads", "sidePanel"],
8+
"permissions": ["activeTab", "scripting", "downloads", "sidePanel", "storage"],
99
"host_permissions": [
1010
"<all_urls>"
1111
],

src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { FabricNav } from './components/navigation';
88
import HomePage from './pages/home/homePage';
99
import ScriptLinks from './pages/scriptlinks';
1010
import { Route, Routes, HashRouter } from 'react-router-dom';
11+
import { trackDevToolsOpen } from './services/analytics';
1112

1213
/* Core CSS required for Ionic components to work properly */
1314
import '@ionic/react/css/core.css';
@@ -89,6 +90,9 @@ const App = () => {
8990
dispatch(setDarkMode(shouldAdd));
9091
};
9192

93+
// Track DevTools extension opened
94+
trackDevToolsOpen();
95+
9296
// toggleDarkTheme(prefersDark.matches)
9397
// this will set the theme according the system preferences
9498
// now we default to dark (true)

src/components/navigation.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
77

88
import { IRootState } from '../store';
99
import { setDarkMode, setLoading, setTheme } from '../store/home/actions';
10+
import { trackFeatureNavigation } from '../services/analytics';
1011

1112
export const FabricNav = () => {
1213
const navigate = useNavigate();
@@ -77,9 +78,9 @@ export const FabricNav = () => {
7778
},
7879
{ name: 'Query Builder', url: '/queryBuilder', key: 'queryBuilderKey', disabled: false },
7980
{ name: 'Site Templates', url: '/siteprovisioning', key: 'siteprovisioningKey', disabled: false },
80-
{ name: 'Page editor', url: '/pageeditor', key: 'pageeditorKey', disabled: false },
81+
/* { name: 'Page editor', url: '/pageeditor', key: 'pageeditorKey', disabled: false },
8182
{ name: 'Modern properties', url: '/modernproperties', key: 'modernpropertiesKey', disabled: false },
82-
{ name: 'App catalog', url: '/appcatalog', key: 'appcatalogKey', disabled: false },
83+
{ name: 'App catalog', url: '/appcatalog', key: 'appcatalogKey', disabled: false },*/
8384
],
8485
[expandedGroups]
8586
);
@@ -193,6 +194,8 @@ export const FabricNav = () => {
193194
menu && menu.close();
194195
if (element.key && selectedKey !== element.key) {
195196
dispatch(setLoading(false));
197+
// Track feature navigation
198+
trackFeatureNavigation(element.key.replace('Key', ''), element.name);
196199
navigate(element.url);
197200
setSelectedKey(element.key);
198201
}

src/popup/Components/QuickLinkButton.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { CommandBarButton, IButtonStyles } from '@fluentui/react';
2+
import { trackPopupLinkClick } from '../../services/analytics';
23

34
export interface IQuickLinkButtonProps {
45
text: string,
@@ -18,16 +19,23 @@ export const buttonStyles: IButtonStyles = {
1819
},
1920
};
2021

22+
// Convert text to a snake_case link name for tracking
23+
const toLinkName = (text: string): string =>
24+
text.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
25+
2126
const QuickLinkButton = ({ text, iconName, disabled, url, newWTab = true }: IQuickLinkButtonProps) => {
2227
return (
2328
<CommandBarButton
2429
text={text}
2530
iconProps={{ iconName: iconName }}
2631
styles={buttonStyles}
2732
disabled={disabled}
28-
onClick={() => newWTab ?
29-
chrome.tabs.create({ url: url }) :
30-
chrome.tabs.update({ url: url })}
33+
onClick={() => {
34+
trackPopupLinkClick(toLinkName(text));
35+
newWTab ?
36+
chrome.tabs.create({ url: url }) :
37+
chrome.tabs.update({ url: url })
38+
}}
3139
/>
3240
)
3341
}

src/popup/PopUp.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Actions from './Components/Actions';
77
import ContextInfoPropertiesList, { ICtxInfoProperty } from './Components/ContextInfoPropertiesList';
88
import QuickLinkList from './Components/QuickLinkList';
99
import LoadTeamsDebug from './Components/LoadTeamsDebug';
10+
import { trackPopupOpen } from '../services/analytics';
1011

1112
//initializeIcons();
1213

@@ -168,6 +169,9 @@ const PopUp = () => {
168169

169170
// load initial data
170171
useEffect(() => {
172+
// Track popup opened
173+
trackPopupOpen();
174+
171175
chrome.tabs.query({ currentWindow: true, active: true }, (tabs: any) => {
172176
const currentTabId = tabs[0].id;
173177
setTabId(currentTabId);

src/services/analytics.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* Simple Google Analytics 4 Measurement Protocol Service
3+
* Tracks: extension opens, feature navigation, popup link clicks
4+
*/
5+
6+
// GA4 Configuration - Set via environment variables
7+
const GA_MEASUREMENT_ID = process.env.REACT_APP_GA_MEASUREMENT_ID || '';
8+
const GA_API_SECRET = process.env.REACT_APP_GA_API_SECRET || '';
9+
10+
const GA_ENDPOINT = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
11+
12+
const CLIENT_ID_KEY = 'sp_editor_ga_client_id';
13+
14+
/**
15+
* Get or generate a persistent client ID
16+
*/
17+
async function getClientId(): Promise<string> {
18+
return new Promise((resolve) => {
19+
if (typeof chrome !== 'undefined' && chrome.storage?.local) {
20+
chrome.storage.local.get([CLIENT_ID_KEY], (result) => {
21+
if (result[CLIENT_ID_KEY]) {
22+
resolve(result[CLIENT_ID_KEY]);
23+
} else {
24+
const newClientId = crypto.randomUUID();
25+
chrome.storage.local.set({ [CLIENT_ID_KEY]: newClientId });
26+
resolve(newClientId);
27+
}
28+
});
29+
} else {
30+
// Fallback for development
31+
let clientId = localStorage.getItem(CLIENT_ID_KEY);
32+
if (!clientId) {
33+
clientId = crypto.randomUUID();
34+
localStorage.setItem(CLIENT_ID_KEY, clientId);
35+
}
36+
resolve(clientId);
37+
}
38+
});
39+
}
40+
41+
/**
42+
* Get the extension ID and browser type
43+
*/
44+
function getExtensionInfo(): { extensionId: string; browser: string } {
45+
const extensionId = typeof chrome !== 'undefined' && chrome.runtime?.id
46+
? chrome.runtime.id
47+
: 'development';
48+
49+
// Detect browser based on user agent
50+
const ua = navigator.userAgent;
51+
let browser = 'unknown';
52+
if (ua.includes('Edg/')) {
53+
browser = 'edge';
54+
} else if (ua.includes('Chrome/')) {
55+
browser = 'chrome';
56+
}
57+
58+
return { extensionId, browser };
59+
}
60+
61+
/**
62+
* Send event to GA4
63+
*/
64+
async function sendEvent(eventName: string, params: Record<string, string | number> = {}): Promise<void> {
65+
try {
66+
const clientId = await getClientId();
67+
const { extensionId, browser } = getExtensionInfo();
68+
69+
const payload = {
70+
client_id: clientId,
71+
events: [{
72+
name: eventName,
73+
params: {
74+
...params,
75+
extension_id: extensionId,
76+
browser: browser,
77+
page_location: `chrome-extension://${extensionId}`,
78+
engagement_time_msec: 100,
79+
}
80+
}]
81+
};
82+
83+
if (navigator.sendBeacon) {
84+
navigator.sendBeacon(GA_ENDPOINT, JSON.stringify(payload));
85+
} else {
86+
fetch(GA_ENDPOINT, {
87+
method: 'POST',
88+
body: JSON.stringify(payload),
89+
keepalive: true,
90+
});
91+
}
92+
} catch {
93+
// Silently fail - analytics should never break the app
94+
}
95+
}
96+
97+
// ============================================================================
98+
// Public API
99+
// ============================================================================
100+
101+
/**
102+
* Track when DevTools extension is opened
103+
*/
104+
export function trackDevToolsOpen(): void {
105+
sendEvent('devtools_open');
106+
}
107+
108+
/**
109+
* Track when Popup extension is opened
110+
*/
111+
export function trackPopupOpen(): void {
112+
sendEvent('popup_open');
113+
}
114+
115+
/**
116+
* Track navigation to a feature in DevTools
117+
* @param featureName - The feature being accessed (e.g., 'pnpjsconsole', 'search')
118+
* @param featureTitle - Human readable title (e.g., 'PnP JS Console')
119+
*/
120+
export function trackFeatureNavigation(featureName: string, featureTitle: string): void {
121+
sendEvent('feature_view', {
122+
feature_name: featureName,
123+
feature_title: featureTitle,
124+
});
125+
}
126+
127+
/**
128+
* Track link click in Popup
129+
* @param linkName - The link identifier (e.g., 'admin_center', 'site_settings')
130+
*/
131+
export function trackPopupLinkClick(linkName: string): void {
132+
sendEvent('popup_link_click', {
133+
link_name: linkName,
134+
});
135+
}

0 commit comments

Comments
 (0)