@equinor/fusion-framework-vite-plugin-spa 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,5 +15,5 @@
15
15
  * @constant
16
16
  * @type {string}
17
17
  */
18
- export declare const html = "\n <!DOCTYPE html>\n <html>\n <head>\n <title>%FUSION_SPA_TITLE%</title>\n <meta name=\"mode\" content=\"%MODE%\">\n <meta name=\"fusion-spa-plugin-version\" content=\"1.1.2\">\n <link rel=\"stylesheet\" href=\"https://cdn.eds.equinor.com/font/equinor-font.css\" />\n <script type=\"module\" src=\"%FUSION_SPA_BOOTSTRAP%\"></script>\n <script>\n // suppress console error for custom elements already defined. \n // WebComponents should be added by the portal, but not removed from application\n const _customElementsDefine = window.customElements.define;\n window.customElements.define = (name, cl, conf) => {\n if (!customElements.get(name)) {\n _customElementsDefine.call(window.customElements, name, cl, conf);\n }\n };\n </script>\n <style>\n html, body {\n margin: 0;\n padding: 0;\n height: 100%;\n font-family: 'EquinorFont', sans-serif;\n }\n </style>\n </head>\n <body></body>\n </html>\n";
18
+ export declare const html = "\n <!DOCTYPE html>\n <html>\n <head>\n <title>%FUSION_SPA_TITLE%</title>\n <meta name=\"mode\" content=\"%MODE%\">\n <meta name=\"fusion-spa-plugin-version\" content=\"1.1.4\">\n <link rel=\"stylesheet\" href=\"https://cdn.eds.equinor.com/font/equinor-font.css\" />\n <script type=\"module\" src=\"%FUSION_SPA_BOOTSTRAP%\"></script>\n <script>\n // suppress console error for custom elements already defined. \n // WebComponents should be added by the portal, but not removed from application\n const _customElementsDefine = window.customElements.define;\n window.customElements.define = (name, cl, conf) => {\n if (!customElements.get(name)) {\n _customElementsDefine.call(window.customElements, name, cl, conf);\n }\n };\n </script>\n <style>\n html, body {\n margin: 0;\n padding: 0;\n height: 100%;\n font-family: 'EquinorFont', sans-serif;\n }\n </style>\n </head>\n <body></body>\n </html>\n";
19
19
  export default html;
@@ -1,3 +1,4 @@
1
1
  import type { ModulesInstance } from '@equinor/fusion-framework-module';
2
2
  import type { MsalModule } from '@equinor/fusion-framework-module-msal';
3
- export declare function registerServiceWorker(framework: ModulesInstance<[MsalModule]>): Promise<void>;
3
+ import { type TelemetryModule } from '@equinor/fusion-framework-module-telemetry';
4
+ export declare function registerServiceWorker(framework: ModulesInstance<[MsalModule, TelemetryModule]>): Promise<void>;
@@ -13,6 +13,9 @@ export type FusionTemplateEnv = {
13
13
  title: string;
14
14
  /** Template bootstrap file path */
15
15
  bootstrap: string;
16
+ telemetry?: {
17
+ consoleLevel?: number;
18
+ };
16
19
  /** Id of the portal to load */
17
20
  portal: {
18
21
  id: string;
@@ -1 +1 @@
1
- export declare const version = "1.1.2";
1
+ export declare const version = "1.1.4";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equinor/fusion-framework-vite-plugin-spa",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Vite plugin for SPA development",
5
5
  "type": "module",
6
6
  "types": "dist/types/index.d.ts",
@@ -46,11 +46,12 @@
46
46
  "@types/lodash.mergewith": "^4.6.2",
47
47
  "rollup": "^4.39.0",
48
48
  "typescript": "^5.8.2",
49
- "vite": "^7.1.5",
50
- "@equinor/fusion-framework-module": "^5.0.2",
51
- "@equinor/fusion-framework-module-http": "^7.0.1",
52
- "@equinor/fusion-framework-module-msal": "^5.0.1",
53
- "@equinor/fusion-framework-module-service-discovery": "^9.0.1"
49
+ "vite": "^7.1.9",
50
+ "@equinor/fusion-framework-module": "5.0.3",
51
+ "@equinor/fusion-framework-module-http": "7.0.2",
52
+ "@equinor/fusion-framework-module-msal": "5.0.1",
53
+ "@equinor/fusion-framework-module-telemetry": "4.2.0",
54
+ "@equinor/fusion-framework-module-service-discovery": "9.0.2"
54
55
  },
55
56
  "scripts": {
56
57
  "build": "tsc -b",
@@ -6,8 +6,18 @@ import {
6
6
  enableServiceDiscovery,
7
7
  type ServiceDiscoveryModule,
8
8
  } from '@equinor/fusion-framework-module-service-discovery';
9
+
10
+ import {
11
+ enableTelemetry,
12
+ TelemetryLevel,
13
+ type TelemetryModule,
14
+ } from '@equinor/fusion-framework-module-telemetry';
15
+ import { ConsoleAdapter } from '@equinor/fusion-framework-module-telemetry/console-adapter';
16
+
9
17
  import { registerServiceWorker } from './register-service-worker.js';
10
18
 
19
+ import { version } from '../version.js';
20
+
11
21
  // @todo - add type for portal manifest when available
12
22
  type PortalManifest = {
13
23
  build: {
@@ -23,8 +33,6 @@ const importWithoutVite = <T>(path: string): Promise<T> => import(/* @vite-ignor
23
33
  // Create Fusion Framework configurator
24
34
  const configurator = new ModulesConfigurator();
25
35
 
26
- configurator.logger.level = import.meta.env.FUSION_SPA_LOG_LEVEL ?? 1;
27
-
28
36
  const serviceDiscoveryUrl = new URL(
29
37
  import.meta.env.FUSION_SPA_SERVICE_DISCOVERY_URL,
30
38
  import.meta.env.FUSION_SPA_SERVICE_DISCOVERY_URL.startsWith('http')
@@ -56,12 +64,65 @@ enableMSAL(configurator, (builder) => {
56
64
  builder.setRequiresAuth(Boolean(import.meta.env.FUSION_SPA_MSAL_REQUIRES_AUTH));
57
65
  });
58
66
 
67
+ enableTelemetry(configurator, {
68
+ attachConfiguratorEvents: true,
69
+ configure: (builder) => {
70
+ const consoleLevel = Number(
71
+ import.meta.env.FUSION_SPA_TELEMETRY_CONSOLE_LEVEL ?? TelemetryLevel.Information,
72
+ );
73
+
74
+ if (Number.isNaN(consoleLevel)) {
75
+ // If environment variable is set but invalid, log all telemetry
76
+ builder.setAdapter(new ConsoleAdapter());
77
+ } else {
78
+ builder.setAdapter(
79
+ new ConsoleAdapter({
80
+ filter: (item) => item.level >= consoleLevel,
81
+ }),
82
+ );
83
+ }
84
+ builder.setMetadata(({ modules }) => {
85
+ const metadata = {
86
+ fusion: {
87
+ spa: {
88
+ version,
89
+ },
90
+ },
91
+ // biome-ignore lint/suspicious/noExplicitAny: we need to use any here to allow dynamic properties
92
+ } as Record<string, any>;
93
+ if (modules?.auth) {
94
+ metadata.fusion.user = {
95
+ id: modules.auth.defaultAccount?.homeAccountId,
96
+ name: modules.auth.defaultAccount?.name,
97
+ email: modules.auth.defaultAccount?.username,
98
+ };
99
+ }
100
+ return metadata;
101
+ });
102
+ },
103
+ });
104
+
59
105
  (async () => {
60
106
  // initialize the framework - this will create the framework instance and configure the modules
61
- const ref = await configurator.initialize<[ServiceDiscoveryModule, HttpModule, MsalModule]>();
107
+ const ref =
108
+ await configurator.initialize<
109
+ [ServiceDiscoveryModule, HttpModule, MsalModule, TelemetryModule]
110
+ >();
111
+
112
+ const telemetry = ref.telemetry;
62
113
 
63
114
  // attach service discovery to the framework - append auth token to configured endpoints
64
- await registerServiceWorker(ref);
115
+ using measurement = telemetry.measure({
116
+ name: 'bootstrap',
117
+ level: TelemetryLevel.Information,
118
+ });
119
+
120
+ await measurement.clone().resolve(registerServiceWorker(ref), {
121
+ data: {
122
+ level: TelemetryLevel.Debug,
123
+ name: 'bootstrap::registerServiceWorker',
124
+ },
125
+ });
65
126
 
66
127
  // create a client for the portal service - this is used to fetch the portal manifest
67
128
  const portalClient = await ref.serviceDiscovery.createClient('portal-config');
@@ -69,11 +130,34 @@ enableMSAL(configurator, (builder) => {
69
130
  // fetch the portal manifest - this is used to load the portal template
70
131
  const portalId = import.meta.env.FUSION_SPA_PORTAL_ID;
71
132
  const portalTag = import.meta.env.FUSION_SPA_PORTAL_TAG ?? 'latest';
72
- const portal_manifest = await portalClient.json<PortalManifest>(
73
- `/portals/${portalId}@${portalTag}`,
74
- );
75
-
76
- const portal_config = await portalClient.json(`/portals/${portalId}@${portalTag}/config`);
133
+ const portal_manifest = await measurement
134
+ .clone()
135
+ .resolve(portalClient.json<PortalManifest>(`/portals/${portalId}@${portalTag}`), {
136
+ data: (manifest: PortalManifest) => ({
137
+ name: 'bootstrap::loadPortalManifest',
138
+ level: TelemetryLevel.Debug,
139
+ properties: {
140
+ portalId,
141
+ portalTag,
142
+ templateEntry: manifest.build.templateEntry,
143
+ manifestVersion: manifest.build.config?.version,
144
+ assetPath: manifest.build.assetPath,
145
+ },
146
+ }),
147
+ });
148
+
149
+ const portal_config = await measurement
150
+ .clone()
151
+ .resolve(portalClient.json(`/portals/${portalId}@${portalTag}/config`), {
152
+ data: {
153
+ name: 'bootstrap::loadPortalConfig',
154
+ level: TelemetryLevel.Debug,
155
+ properties: {
156
+ portalId,
157
+ portalTag,
158
+ },
159
+ },
160
+ });
77
161
 
78
162
  // create a entrypoint for the portal - this is used to render the portal
79
163
  const el = document.createElement('div');
@@ -86,8 +170,22 @@ enableMSAL(configurator, (builder) => {
86
170
 
87
171
  // @todo: should test if the entrypoint is external or internal
88
172
  // @todo: add proper return type
89
- const { render } =
90
- await importWithoutVite<Promise<{ render: (...args: unknown[]) => void }>>(portalEntryPoint);
173
+ const { render } = await measurement
174
+ .clone()
175
+ .resolve(
176
+ importWithoutVite<Promise<{ render: (...args: unknown[]) => void }>>(portalEntryPoint),
177
+ {
178
+ data: {
179
+ name: 'bootstrap::loadPortalSourceCode',
180
+ level: TelemetryLevel.Debug,
181
+ properties: {
182
+ portalId,
183
+ portalTag,
184
+ entryPoint: portalEntryPoint,
185
+ },
186
+ },
187
+ },
188
+ );
91
189
 
92
190
  // render the portal - this will load the portal template and render it
93
191
  render(el, { ref, manifest: portal_manifest, config: portal_config });
@@ -1,16 +1,30 @@
1
1
  import type { ModulesInstance } from '@equinor/fusion-framework-module';
2
2
  import type { MsalModule } from '@equinor/fusion-framework-module-msal';
3
+ import { TelemetryLevel, type TelemetryModule } from '@equinor/fusion-framework-module-telemetry';
3
4
 
4
- export async function registerServiceWorker(framework: ModulesInstance<[MsalModule]>) {
5
+ export async function registerServiceWorker(
6
+ framework: ModulesInstance<[MsalModule, TelemetryModule]>,
7
+ ) {
8
+ const telemetry = framework.telemetry;
5
9
  if ('serviceWorker' in navigator === false) {
6
- console.warn('Service workers are not supported in this browser.');
7
- return;
10
+ const exception = new Error('Service workers are not supported in this browser.');
11
+ exception.name = 'ServiceWorkerNotSupported';
12
+ telemetry.trackException({
13
+ name: `registerServiceWorker.${exception.name}`,
14
+ exception,
15
+ });
16
+ throw exception;
8
17
  }
9
18
 
10
19
  const resourceConfigs = import.meta.env.FUSION_SPA_SERVICE_WORKER_RESOURCES;
11
20
  if (!resourceConfigs) {
12
- console.warn('Service worker config is not defined.');
13
- return;
21
+ const exception = new Error('Service worker config is not defined.');
22
+ exception.name = 'ServiceWorkerConfigNotDefined';
23
+ telemetry.trackException({
24
+ name: `registerServiceWorker.${exception.name}`,
25
+ exception,
26
+ });
27
+ throw exception;
14
28
  }
15
29
 
16
30
  /**
@@ -34,14 +48,19 @@ export async function registerServiceWorker(framework: ModulesInstance<[MsalModu
34
48
  // extract scopes from the event data
35
49
  const scopes = event.data.scopes as string[];
36
50
  if (!scopes || !Array.isArray(scopes)) {
37
- throw new Error('Invalid scopes provided');
51
+ const error = new Error('Invalid scopes provided');
52
+ error.name = 'InvalidScopesProvided';
53
+ throw error;
38
54
  }
39
55
 
40
56
  // request a token from the MSAL module
41
57
  const token = await framework.auth.acquireToken({ scopes });
42
58
 
43
59
  if (!token) {
44
- throw new Error('Failed to acquire token');
60
+ const error = new Error('Failed to acquire token');
61
+ error.name = 'FailedToAcquireToken';
62
+
63
+ throw error;
45
64
  }
46
65
 
47
66
  // send the token back to the service worker
@@ -50,6 +69,11 @@ export async function registerServiceWorker(framework: ModulesInstance<[MsalModu
50
69
  expiresOn: token.expiresOn?.getTime(),
51
70
  });
52
71
  } catch (error) {
72
+ const exception = error as Error;
73
+ telemetry.trackException({
74
+ name: `serviceWorker.onMessage.${exception.name}`,
75
+ exception,
76
+ });
53
77
  event.ports[0].postMessage({
54
78
  error: (error as Error).message,
55
79
  });
@@ -57,14 +81,26 @@ export async function registerServiceWorker(framework: ModulesInstance<[MsalModu
57
81
  }
58
82
  });
59
83
 
60
- // register the service worker
84
+ // register the service worker with telemetry
61
85
  // updateViaCache: 'none' ensures the service worker script is always fetched fresh
62
86
  // This is important during development to pick up code changes
63
- const registration = await navigator.serviceWorker.register('/@fusion-spa-sw.js', {
64
- type: 'module',
65
- scope: '/',
66
- updateViaCache: 'none',
87
+ using measurement = telemetry.measure({
88
+ name: 'registerServiceWorker',
89
+ level: TelemetryLevel.Information,
67
90
  });
91
+ const registration = await measurement.clone().resolve(
92
+ navigator.serviceWorker.register('/@fusion-spa-sw.js', {
93
+ type: 'module',
94
+ scope: '/',
95
+ updateViaCache: 'none',
96
+ }),
97
+ {
98
+ data: {
99
+ name: 'registerServiceWorker.register',
100
+ level: TelemetryLevel.Debug,
101
+ },
102
+ },
103
+ );
68
104
 
69
105
  // Handle service worker updates/installations
70
106
  // If there's a service worker waiting or installing, send config when it activates
@@ -88,7 +124,12 @@ export async function registerServiceWorker(framework: ModulesInstance<[MsalModu
88
124
  });
89
125
 
90
126
  // wait for the service worker to be ready
91
- const readyRegistration = await navigator.serviceWorker.ready;
127
+ const readyRegistration = await measurement.clone().resolve(navigator.serviceWorker.ready, {
128
+ data: {
129
+ name: 'registerServiceWorker.ready',
130
+ level: TelemetryLevel.Debug,
131
+ },
132
+ });
92
133
 
93
134
  // ensure we have an active service worker before sending config
94
135
  const activeWorker = readyRegistration.active;
@@ -100,27 +141,35 @@ export async function registerServiceWorker(framework: ModulesInstance<[MsalModu
100
141
  // CRITICAL: Wait for the service worker to become the controller
101
142
  // This ensures the service worker can intercept fetch requests
102
143
  if (!navigator.serviceWorker.controller) {
103
- await new Promise<void>((resolve) => {
104
- let checkInterval: NodeJS.Timeout;
105
-
106
- const finish = () => {
107
- clearInterval(checkInterval);
108
- navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
109
- resolve();
110
- };
111
-
112
- const onControllerChange = () => finish();
113
-
114
- // If controllerchange fires, the service worker has taken control
115
- navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
116
-
117
- // Polling fallback and timeout to prevent infinite waiting
118
- checkInterval = setInterval(() => {
119
- if (navigator.serviceWorker.controller) finish();
120
- }, 200);
121
-
122
- setTimeout(finish, 5000);
123
- });
144
+ await measurement.clone().resolve(
145
+ new Promise<void>((resolve) => {
146
+ let checkInterval: NodeJS.Timeout;
147
+
148
+ const finish = () => {
149
+ clearInterval(checkInterval);
150
+ navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
151
+ resolve();
152
+ };
153
+
154
+ const onControllerChange = () => finish();
155
+
156
+ // If controllerchange fires, the service worker has taken control
157
+ navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
158
+
159
+ // Polling fallback and timeout to prevent infinite waiting
160
+ checkInterval = setInterval(() => {
161
+ if (navigator.serviceWorker.controller) finish();
162
+ }, 200);
163
+
164
+ setTimeout(finish, 5000);
165
+ }),
166
+ {
167
+ data: {
168
+ name: 'registerServiceWorker.controllerWait',
169
+ level: TelemetryLevel.Debug,
170
+ },
171
+ },
172
+ );
124
173
  }
125
174
 
126
175
  // send the config to the active service worker
@@ -131,6 +180,9 @@ export async function registerServiceWorker(framework: ModulesInstance<[MsalModu
131
180
  sendConfigToServiceWorker(navigator.serviceWorker.controller);
132
181
  }
133
182
  } catch (error) {
134
- console.error('Service Worker registration failed:', error);
183
+ telemetry.trackException({
184
+ name: `registerServiceWorker.${(error as Error).name}`,
185
+ exception: error as Error,
186
+ });
135
187
  }
136
188
  }
package/src/types.ts CHANGED
@@ -17,6 +17,10 @@ export type FusionTemplateEnv = {
17
17
  /** Template bootstrap file path */
18
18
  bootstrap: string;
19
19
 
20
+ telemetry?: {
21
+ consoleLevel?: number;
22
+ };
23
+
20
24
  /** Id of the portal to load */
21
25
  portal: {
22
26
  id: string;
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '1.1.2';
2
+ export const version = '1.1.4';
package/tsconfig.json CHANGED
@@ -21,6 +21,9 @@
21
21
  {
22
22
  "path": "../../modules/service-discovery"
23
23
  },
24
+ {
25
+ "path": "../../modules/telemetry"
26
+ },
24
27
  {
25
28
  "path": "../../modules/services"
26
29
  }