How can I utilise ApplicationInsights-JS in a service worker? - reactjs

I am currently using ApplicationInsights-JS in my progressive web app. It works in my react components as I can import what I need from the relevant npm packages.
In my service worker however, I can only import logic using importScripts.
I did manage to find a CDN for ApplicationInsights-JS on their Github page however it seems that in order to initialise app insights using this library you need to have access to window in order to store the appinsights, which you cannot do from a service worker.
I tried to use the web snippet approach since the CDN seemed to be
related to that particular library, but I can't use window and am not sure how else to implement this solution.
This is a copy paste of the suggested snippet to init the app insights object from: https://github.com/Microsoft/ApplicationInsights-JS
importScripts('https://az416426.vo.msecnd.net/beta/ai.2.min.js');
const sdkInstance = 'appInsightsSDK';
window[sdkInstance] = 'appInsights';
const aiName = window[sdkInstance];
const aisdk =
window[aiName] ||
(function(e) {
function n(e) {
i[e] = function() {
const n = arguments;
i.queue.push(function() {
i[e](...n);
});
};
}
let i = { config: e };
i.initialize = !0;
const a = document;
const t = window;
setTimeout(function() {
const n = a.createElement('script');
(n.src = e.url || 'https://az416426.vo.msecnd.net/next/ai.2.min.js'),
a.getElementsByTagName('script')[0].parentNode.appendChild(n);
});
try {
i.cookie = a.cookie;
} catch (e) {}
(i.queue = []), (i.version = 2);
for (
const r = [
'Event',
'PageView',
'Exception',
'Trace',
'DependencyData',
'Metric',
'PageViewPerformance'
];
r.length;
)
n(`track${r.pop()}`);
n('startTrackPage'), n('stopTrackPage');
const o = `Track${r[0]}`;
if (
(n(`start${o}`),
n(`stop${o}`),
!(
!0 === e.disableExceptionTracking ||
(e.extensionConfig &&
e.extensionConfig.ApplicationInsightsAnalytics &&
!0 ===
e.extensionConfig.ApplicationInsightsAnalytics
.disableExceptionTracking)
))
) {
n(`_${(r = 'onerror')}`);
const s = t[r];
(t[r] = function(e, n, a, t, o) {
const c = s && s(e, n, a, t, o);
return (
!0 !== c &&
i[`_${r}`]({
message: e,
url: n,
lineNumber: a,
columnNumber: t,
error: o
}),
c
);
}),
(e.autoExceptionInstrumented = !0);
}
return i;
})({ instrumentationKey: 'xxx-xxx-xxx-xxx-xxx' });
(window[aiName] = aisdk),
aisdk.queue && aisdk.queue.length === 0 && aisdk.trackPageView({});
I get window is not defined which is expected, but I'm not sure how else I can make use of this library from the service worker.
Has anyone else had a similar implementation in which they successfully logged telemetry using ApplicationInsights from a service worker?

I realised that I was over complicating this.
Since I only needed to track a custom event, and didn't need all the automated page tracking etc that appInsights does, I ended up doing a fetch from my service worker.
I just copied the header and body format from the requests that I made using my react pages.
The below successfully logged telemetry to my app insights dashboard:
fetch(url, {
method: 'post',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify([
{
time: '2019-05-02T15:56:37.589Z',
iKey: 'INSTRUMENTATION_KEY',
name:
'Microsoft.ApplicationInsights.INSTRUMENTATION_KEY.Event',
tags: {
'ai.user.id': 'l6Tey',
'ai.session.id': 'TL+Ry',
'ai.device.id': 'browser',
'ai.device.type': 'Browser',
'ai.operation.id': 'HUfNE',
SampleRate: '100',
// eslint-disable-next-line no-script-url
'ai.internal.sdkVersion': 'javascript:2.0.0-rc4'
},
data: {
baseType: 'EventData',
baseData: {
ver: 2,
name: 'Testing manual event',
properties: {},
measurements: {}
}
}
}
])
})
.then(json)
.then(function(data) {
})
.catch(function(error) {
});

I've almost managed to use Microsoft Application Insights in our app's service worker.
The key parts are:
Using the lightweight version of appInsights (see this small remark at 4th step) with importScripts('https://az416426.vo.msecnd.net/next/aib.2.min.js').
Initialize an appInsights object:
appInsights = new Microsoft.AppInsights.AppInsights({ instrumentationKey: "[replace with your own key]" });
when track needed (during onpush event or onnotificationclick), go for appInsight.track({ eventItemFields }) then appInsights.flush().
I've said "almost" because the flush part seems to not working, I've got: "Sender was not initialized" internal error after enabling debugging.
I will publish here a working sample code if I successfully manage this issue.
References:
https://github.com/Azure-Samples/applicationinsights-web-sample1/blob/master/testlightsku.html
This response to the question: How to add analytics for Push notifications.

Using the Web SDK in a service worker is troublesome. The full version depends on a window object, while the basic SDK depends on Beacon or XmlHttpRequest for sending the messages (in file https://github.com/microsoft/ApplicationInsights-JS/blob/master/channels/applicationinsights-channel-js/src/Sender.ts):
if (!_self._senderConfig.isBeaconApiDisabled() && Util.IsBeaconApiSupported()) {
_self._sender = _beaconSender;
} else {
if (typeof XMLHttpRequest !== undefined) {
const xhr:any = getGlobalInst("XMLHttpRequest");
if(xhr) {
const testXhr = new xhr();
if ("withCredentials" in testXhr) {
_self._sender = _xhrSender;
_self._XMLHttpRequestSupported = true;
} else if (typeof XDomainRequest !== undefined) {
_self._sender = _xdrSender; // IE 8 and 9
}
}
}
}
At the moment Application Insights SDK does not seem to support service workers. Rajars solution seems to be the best option for now.
Update: There is an issue in the Github Repo about this: https://github.com/microsoft/ApplicationInsights-JS/issues/1436
A suggestion that works is by using the basic/lightweight version of Application Insights (as mentioned by Rajar) and adding a XMLHttpRequest polyfill (that uses the fetch api) before inititializing Application Insights. After that you can use the lightweight version.
An example can be found here: https://github.com/Pkiri/pwa-ai

I was trying to use AppInsightsSDK in E2E tests environment (pupeteer) and when I tried to log event or metric I got with "Sender was not initialized" error.
As #Pkiri mentioned one would need XMLHttpRequest polyfill to solve the issue. Although my scenario is not directly related to Service worker I wanted to mention that #Pkiri answer is not entirely true, because one can also use globalThis, self, window or global to get the same result according to SDK source code function getGlobalInst("XMLHttpRequest"); resolves to
function getGlobal() {
if (typeof globalThis !== strShimUndefined && globalThis) {
return globalThis;
}
if (typeof self !== strShimUndefined && self) {
return self;
}
if (typeof window !== strShimUndefined && window) {
return window;
}
if (typeof global !== strShimUndefined && global) {
return global;
}
return null;
}
And for my scenario this was a valid solution
const appInsights = new ApplicationInsights({
config: {
instrumentationKey: 'AppInsights_InstrumentationKey',
},
});
global.XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
global.appInsights = appInsights.loadAppInsights();

Related

React 17 useMemo create a losted instance of object

[EDIT]
Thanks to #dbuchet for his anwser. The problem comes from the fact that in StrictMode in dev mode only, everythings is running twice.
[PROBLEM]
I just got a weird beavour in React.
My context :
I want to create an EventSource for work with Mercure. So i created a hook and this hook store my EventSource with a useMemo. Until here everything's is fine. But after some tests, eachtime i call my hook, two connections are created not only one.
The problem is that i am not able to close the first connection. Only the second. This cause memory leak and undesired persistent connections.
My useMemo for the EventSource :
const eventSource = useMemo(() => {
if (topics.length === 0 || !hubUrl || !baseUrl) {
return null;
}
let url = new URL(hubUrl);
topics.forEach(topic => {
url.searchParams.append('topic', (baseUrl ? baseUrl : '') + topic);
});
return new EventSource(url, {
withCredentials: withCredentials
});
}, [baseUrl, hubUrl, withCredentials, topicsHash]);
After some investigations, i can see that if i add some Math.random(), logs and a simple setInterval, i am able to see that two different objects are created :
const eventSource = useMemo(() => {
if (topics.length === 0 || !hubUrl || !baseUrl) {
return null;
}
let url = new URL(hubUrl);
topics.forEach(topic => {
url.searchParams.append('topic', (baseUrl ? baseUrl : '') + topic);
});
console.log('opening... ' + temp)
let connectionInterval = null;
connectionInterval = setInterval(() => {
console.log('------------- setInterval ---------------')
onsole.log(topics, eventSource, temp);
clearInterval(connectionInterval);
});
let test = { test: temp, eventSource: new EventSource(url, {
withCredentials: withCredentials
})}
return test;
}, [baseUrl, hubUrl, withCredentials, topicsHash]);
The result in the logger is :
So we can see that my useMemo seems to be called only once (due to the fact that 'opening...' log is visible only once). But the random is different of the final object that i have at the end.
Moreover, setInterval is executed twice with both randoms visible.
Maybe something in my project cause this, so i created a simple peice of React and try the code below and same beavour :
const temp = useMemo(() => {
let test = Math.random();
let inter = setInterval(() => {
console.log('setInterval');
console.log(test);
clearInterval(inter);
});
console.log(test);
}, ['for block reloading'])
Result of the console :
Anyone got the same problem ?
Is there a way to avoid this, maybe i made a mistake somewhere or i misunderstood something with useMemo ?
Thanks !

React websockets disappear on refresh

So I have a websocket connection that is open and I notice once I refresh the page the websocket messages disappears. I've read online that this is supposed to happen. What is a good way to persist these messages so that they do not disappear. Right now I have the websocket messages in a react state. I've seen some say localstorage or cookies, but I don't think that is scalable as their can be thousands of messages in minutes that could overload the browser storage? Below I am using react-use-websocket package and I get the last message and store that inside a state array. That is the wrong approach I need a longer storage solution.
const { lastJsonMessage, lastMessage, sendMessage, readyState, getWebSocket } = useWebSocket(resultUrl, {
//Will attempt to reconnect on all close events, such as server shutting down
shouldReconnect: () => true,
reconnectAttempts: 10,
reconnectInterval: 3000
});
useEffect(() => {
const val = lastJsonMessage ? JSON.parse(lastJsonMessage as unknown as string) : {};
if (val !== null && Object.keys(val).length > 0) {
setMessageHistory((prev) => prev.concat(val));
}
}, [lastJsonMessage, setMessageHistory]);
// Get Video assets after finishing;
useEffect(() => {
messageHistory.forEach((msg) => {
const { type } = msg;
if (type === 'video.live_stream.recording' || type === 'video.live_stream.active') {
const localPlaybackId = msg.data?.playback_ids[0];
setPlaybackId(localPlaybackId);
}
setVideoType(type);
});
}, [messageHistory, videoType]);
const connectionStatus = {
[ReadyState.CONNECTING]: 'Connecting',
[ReadyState.OPEN]: 'Open',
[ReadyState.CLOSING]: 'Closing',
[ReadyState.CLOSED]: 'Closed',
[ReadyState.UNINSTANTIATED]: 'Uninstantiated'
}[readyState];
if (liveStreamId) {
sendMessage(liveStreamId);
}
messageHistory.filter((msg) => {
msg.data.id === liveStreamId;
});
in this case, usually when the app is loaded you should do a GET request to the server in order to load all the last messages. Also since you are talking about "thousands of messages" you should implement a lazy loading aka paginator or infinite-scroll

How to In App Purchases with ReactJS and Capacitor

I am using Capacitor to generate both the IOS and Android apps (not using Iconic) - this works well, but we are trying to implement IAP (for IOS only at this stage) and cannot figure it out.
I have followed various guides (https://ionicframework.com/docs/native/in-app-purchase-2 and https://purchase.cordova.fovea.cc/ and https://capacitorjs.com/docs/guides/in-app-purchases) but simply cannot get it working with React (not React Native)
Can someone point me in the right direction, or provide sample code?
You didn't describe what is going wrong, but here's a basic configuration that works for me on iOS.
I'm only including the part about the store:
index.tsx
import { IAPProduct, InAppPurchase2 } from '#ionic-native/in-app-purchase-2';
const startStoreEventListeners = () => {
if (isPlatformMobile()) {
document.addEventListener(
'deviceready',
() => {
const store = InAppPurchase2;
// Needed to use IAP + cordova plugins.
// Set debug messages.
// Default.
store.verbosity = store.QUIET;
// store.verbosity = store.DEBUG;
store.register([
{
id: subMonthly,
type: store.PAID_SUBSCRIPTION,
},
{
id: subAnnual,
type: store.PAID_SUBSCRIPTION,
},
]);
// Upon approval, verify the receipt.
store.when(subMonthly).approved((product: IAPProduct) => {
product.verify();
});
store.when(subAnnual).approved((product: IAPProduct) => {
product.verify();
});
// Upon receipt validation, mark the subscription as owned.
store.when(subMonthly).verified((product: IAPProduct) => {
product.finish();
});
store.when(subAnnual).verified((product: IAPProduct) => {
product.finish();
});
// Track all store errors
store.error((err: Error) => {
debugLog('Store Error', JSON.stringify(err));
});
// https://billing-dashboard.fovea.cc/setup/cordova
store.validator =
'https://validator.fovea.cc/v1/validate?appName=secret';
store.refresh();
startIonic();
},
false,
);
} else {
startIonic();
}
};
startStoreEventListeners();
serviceWorker.unregister();
Note that #ionic-native packages are deprecated and need to be converted.

Firebase functions not being invoked

I am trying to integrate Stripe payments on my webapp using Firebase. I have cloned the code from the repository here: https://github.com/firebase/functions-samples/tree/master/stripe and have followed the documentation here: https://firebase.google.com/docs/use-cases/payments
From reading the documentation, I assumed that when a customer signed in through firebase authentication, their details would be added to a stripe_customer collection in the firestore. I realised this wasn't the case, and manually added a user to test the save card functions. Then I received the following error : "Invalid value for stripe.confirmCardSetup intent secret: value should be a client_secret string. You specified: undefined"
I have a blaze plan for firebase and have configured. From following the steps in the documentation, I assumed this would be working. I'm sorry this question is so vague, but it seems at every corner I'm getting another issue. Is there something very obvious I am missing that is stopping this code from working? I am trying to implement this for a friends business as a favor, and am getting really confused with Firebase. I am coding in Angularjs. Would greatly appreciate any help on this!
This is the code for the function to create a customer
exports.createStripeCustomer = functions.auth.user().onCreate(async (user) => {
const customer = await stripe.customers.create({ email: user.email });
const intent = await stripe.setupIntents.create({
customer: customer.id,
});
await admin.firestore().collection('stripe_customers').doc(user.uid).set({
customer_id: customer.id,
setup_secret: intent.client_secret,
});
return;
});
And this is the code being called in the controller:
const firebaseUI = new firebaseui.auth.AuthUI(firebase.auth());
const firebaseUiConfig = {
callbacks: {
signInSuccessWithAuthResult: function (authResult, redirectUrl) {
// User successfully signed in.
// Return type determines whether we continue the redirect automatically
// or whether we leave that to developer to handle.
return true;
},
uiShown: () => {
document.getElementById('loader').style.display = 'none';
},
},
signInFlow: 'popup',
signInSuccessUrl: '/checkout.html',
signInOptions: [
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.EmailAuthProvider.PROVIDER_ID,
],
credentialHelper: firebaseui.auth.CredentialHelper.NONE,
// Your terms of service url.
tosUrl: 'https://example.com/terms',
// Your privacy policy url.
privacyPolicyUrl: 'https://example.com/privacy',
};
firebase.auth().onAuthStateChanged((firebaseUser) => {
if (firebaseUser) {
currentUser = firebaseUser;
firebase
.firestore()
.collection('stripe_customers')
.doc(currentUser.uid)
.onSnapshot((snapshot) => {
if (snapshot.data()) {
customerData = snapshot.data();
startDataListeners();
document.getElementById('loader').style.display = 'none';
document.getElementById('content').style.display = 'block';
} else {
console.warn(
`No Stripe customer found in Firestore for user: ${currentUser.uid}`
);
}
});
} else {
document.getElementById('content').style.display = 'none';
firebaseUI.start('#firebaseui-auth-container', firebaseUiConfig);
}
});
The error you've supplied (below) implies that the key in your config isn't been pulled into your code. If you're running this locally you need to run the below any time you change your functions:config values.
firebase functions:config:get > .runtimeconfig.json
Check the doc's out about how to run your function locally:
Error
"Invalid value for stripe.confirmCardSetup intent secret: value should
be a client_secret string. You specified: undefined"

Authentication in Angular 2, handling the observables

I just started with a Angular 2 project and am trying to get authentication up and running. Inspired by this tutorial I decided to do the following:
Create a custom RouterOutlet class (extending it) to handle the authentication logic whenever a url is called.
I succeeded in this custom class, but am still not sure how to check if a user is authenticated. My situation is as follows, I need to query a get call to a external API, for my development proces it is as follows:
getAdmin() {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.get('http://localhost:3000/admin/is_admin.json', options)
.map(res => res)
.catch(this.handleError)
}
This API call returns true or false. I was wondering what would be the best option to use this information? Should I for example call the following function each time a URL should be checked?:
isAdmin() {
this.getAdmin().subscribe(
data => this.authenticationResult = data,
error => console.log("Error: ", error),
() => return JSON.parse(this.authenticationResult._data);
}
I can't get this up and running because my observable is undefined when using the function I gave as example.
The "problem" is that your method is asynchronous so you need to be careful the way and when you use it.
If you want to use within the activate method of your custom RouterOutlet, you need to leverage observables and reactive programming.
I don't know exactly the way you want to check admin roles:
activate(instruction: ComponentInstruction) {
return this.userService.getAdmin().flatMap((isAdmin) => {
if (this.userService.isLoggIn()) {
if (this._canActivate(instruction.urlPath, isAdmin) {
return Observable.fromPromise(super.activate(instruction));
} else {
this.router.navigate(['Forbidden']);
return Observable.throw('Forbidden');
}
} else {
this.router.navigate(['Login']);
return Observable.throw('Not authenticated');
}
}).toPromise();
}
_canActivate(url, admin) {
return this.publicRoutes.indexOf(url) !== -1
|| this.userService.isLoggedIn();
}
In order to optimize the request, you could lazily (and only once) call the request to check if the user is admin or not:
isAdmin:boolean;
getAdmin() {
if (this.isAdmin) {
return Observable.of(this.isAdmin);
} else {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.get('http://localhost:3000/admin/is_admin.json', options)
.map(res => res)
.catch(this.handleError);
}
}
Another approach will be also to load this hint when authenticating the user. This way, the implementation of the activate method would be simplier:
activate(instruction: ComponentInstruction) {
if (this.userService.isLoggIn()) {
if (this.userService.isAdmin()) {
return super.activate(instruction);
} else if (this._canActivate(instruction.urlPath, isAdmin) {
return super.activate(instruction);
} else {
this.router.navigate(['Forbidden']);
}
} else {
this.router.navigate(['Login']);
}
}
_canActivate(url, admin) {
return this.publicRoutes.indexOf(url) !== -1
|| this.userService.isLoggedIn();
}
I would consider to call getAdmin() somehow as first Step of your app, store the result in a SessionService object which you move around using Dependency Injection. This way any time you need to check the result of getAdmin you can ask the SessionService instance.
I hope this helps

Resources