ApplicationInsights-JS icon indicating copy to clipboard operation
ApplicationInsights-JS copied to clipboard

How to share an application insights instance across ASP.NET Core (C#) and React (typescript)?

Open alexnesh opened this issue 2 years ago • 7 comments

Hi,

How do I share an application insights instance that has been created via the .NET application insights package with my typescript frontend?

I know that I could create another instance in our typescript frontend, but that would require me to encode a connection string in our app (unless the typescript application insights package can consume an environment variable, which I've not seen documentation for but I may have missed). I've wired up ApplicationInsights for the .NET part of the codebase by using an environment variable for the connection string, which allows us to use Azure to point the deployed App Service to a particular regional application insights resource, without having to encode any connection strings within the app. The .NET Application Insights package natively knows how to search for this environment variable, hence the only programmatic set-up from the .NET side needed is to add the telemetry as a service.

Now, I would like to be able to "share" this instance with the typescript frontend, such that I can use the same instance for logging in both parts of the codebase. I know that this is possible because we have a legacy web codebase that does just that - but despite my digging and researching I'm unable to determine the correct way to plumb this. There seems to be zero initialization or "wiring" from the typescript side in the legacy codebase, so I'm at a bit of a stand-still.

For context, our controllers, data handlers, etc. all live in .NET-land while our UX lives in React, so I'd like to be able to log from both places.

Any insights (no pun intended) would be much appreciated. Feel free to reach out to me on Teams as well if that's easier! Thanks so much.

  • AJN

alexnesh avatar Mar 15 '23 19:03 alexnesh

Ok, when you enable codeless attach this causes the SDK to be downloaded and initialized from the CDN.

As part of this initialization this creates a global appInsights on window to enable you to use it.

So for you typescript front end you could either change to use this global (assuming that you are using NPM) -- you will probably require some wrappers around the typings for the global or perhaps just declare a global as the main AISku instance and keep using the typing from the npm package.

Probably (I've not tried this), something like

declare var appInsights: IApplicationInsights; Where the IApplicationInsights comes from @microsoft/applicationinsights-web

Or. Disable codeless attach and just use npm, of course as you pointed out you would need to get your connectionString / instrumentationKey into you npm based initialization (assuming you don't already hard code it)

MSNev avatar Mar 15 '23 19:03 MSNev

Thanks for the input @MSNev. Yes, I'm currently using npm to pull the microsoft/applicationinsights-web package.

Right, so the old codebase that does this defines an extension to the global window interface that adds appInsights a la:

declare global { interface Window { ... appInsights: any; ... } }

And just defines a telemetry client a la

private _telemetryClient: IAppInsights = window.appInsights;

Can you elaborate on codeless attach - how do I enable/disable this? When trying to mock what I had seen above, basically the window's appInsights var was undefined, so I assume I'm missing the codeless attach part which does the init that you mentioned above. Additionally, I'm pulling the .NET nuget package as well, and adding the application insights as a service in my Startup.cs via:

public void ConfigureServices(IServiceCollection services) { // add telemetry services.AddApplicationInsightsTelemetry(); ...

alexnesh avatar Mar 15 '23 19:03 alexnesh

The codeless attach is a feature of hosting your application in the Azure portal and I believe that there is a configuration in the UI the enable / disable @Karlie-777, Can you please provide a link to the documentation for this. It works as a filter on the outbound (returned) HTML page where it injects a code snippet onto the page to cause the SDK to be downloaded. It is also possible via a .Net NuGet package (I can't find the links) where in your application you add a small code reference which will cause effectively the same code snippet to be added to the response generated by your server.

If appInsights is undefined then neither of the above are in place (as this script is dropped into the head section of the HTML and adds the global and provides some proxy functions (not the entire SDK API) to capture the SDK calls that are replayed (re-executed) once the SDK is loaded -- basically this means that if your attempting to use some of the SDK functions that are not proxied they will fail (because the functions are undefined) until the SDK is loaded (which may fail if the user has an Ad Blocker installed).

These are the only functions that are proxied which means your _telemetryClient (may) fail if you are calling any other functions and either the SDK has not yet loaded or failed for some reason.

MSNev avatar Mar 16 '23 16:03 MSNev

@alexnesh .NET Core supports client-side monitoring , this link: .NET Core Enable client-side telemetry for web applications might help. But this feature actually injects JavaScript ApplicationInsights instance (with the connection string in your .NET Core app environment variables) via CDN into every returned response instead of "sharing instance".

Karlie-777 avatar Mar 16 '23 16:03 Karlie-777

Great - looking at the docs @Karlie-777 linked, it looks like the snippets need to be included in _ViewImports.cshtml and _Layout.cshtml. I had done the _ViewImports.cshtml snippet, but because this is a React app with ASP just used for pinging services, data management etc., there is no _Layout.cshtml. The document @Karlie-777 linked accounts for this via:

_If your project doesn't include Layout.cshtml, you can still add client-side monitoring by adding the JavaScript snippet to an equivalent file that controls the of all pages within your app.

Looking at the document linked in this comment, it mentions manually adding the following script to your index file:

<script type="text/javascript"> !function(T,l,y){var S=T.location,k="script",D="instrumentationKey",C="ingestionendpoint",I="disableExceptionTracking",E="ai.device.",b="toLowerCase",w="crossOrigin",N="POST",e="appInsightsSDK",t=y.name||"appInsights";(y.name||T[e])&&(T[e]=t);var n=T[t]||function(d){var g=!1,f=!1,m={initialize:!0,queue:[],sv:"5",version:2,config:d};function v(e,t){var n={},a="Browser";return n[E+"id"]=a[b](),n[E+"type"]=a,n["ai.operation.name"]=S&&S.pathname||"_unknown_",n["ai.internal.sdkVersion"]="javascript:snippet_"+(m.sv||m.version),{time:function(){var e=new Date;function t(e){var t=""+e;return 1===t.length&&(t="0"+t),t}return e.getUTCFullYear()+"-"+t(1+e.getUTCMonth())+"-"+t(e.getUTCDate())+"T"+t(e.getUTCHours())+":"+t(e.getUTCMinutes())+":"+t(e.getUTCSeconds())+"."+((e.getUTCMilliseconds()/1e3).toFixed(3)+"").slice(2,5)+"Z"}(),iKey:e,name:"Microsoft.ApplicationInsights."+e.replace(/-/g,"")+"."+t,sampleRate:100,tags:n,data:{baseData:{ver:2}}}}var h=d.url||y.src;if(h){function a(e){var t,n,a,i,r,o,s,c,u,p,l;g=!0,m.queue=[],f||(f=!0,t=h,s=function(){var e={},t=d.connectionString;if(t)for(var n=t.split(";"),a=0;a<n.length;a++){var i=n[a].split("=");2===i.length&&(e[i[0][b]()]=i[1])}if(!e[C]){var r=e.endpointsuffix,o=r?e.location:null;e[C]="https://"+(o?o+".":"")+"dc."+(r||"services.visualstudio.com")}return e}(),c=s[D]||d[D]||"",u=s[C],p=u?u+"/v2/track":d.endpointUrl,(l=[]).push((n="SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details)",a=t,i=p,(o=(r=v(c,"Exception")).data).baseType="ExceptionData",o.baseData.exceptions=[{typeName:"SDKLoadFailed",message:n.replace(/\./g,"-"),hasFullStack:!1,stack:n+"\nSnippet failed to load ["+a+"] -- Telemetry is disabled\nHelp Link: https://go.microsoft.com/fwlink/?linkid=2128109\nHost: "+(S&&S.pathname||"_unknown_")+"\nEndpoint: "+i,parsedStack:[]}],r)),l.push(function(e,t,n,a){var i=v(c,"Message"),r=i.data;r.baseType="MessageData";var o=r.baseData;return o.message='AI (Internal): 99 message:"'+("SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details) ("+n+")").replace(/\"/g,"")+'"',o.properties={endpoint:a},i}(0,0,t,p)),function(e,t){if(JSON){var n=T.fetch;if(n&&!y.useXhr)n(t,{method:N,body:JSON.stringify(e),mode:"cors"});else if(XMLHttpRequest){var a=new XMLHttpRequest;a.open(N,t),a.setRequestHeader("Content-type","application/json"),a.send(JSON.stringify(e))}}}(l,p))}function i(e,t){f||setTimeout(function(){!t&&m.core||a()},500)}var e=function(){var n=l.createElement(k);n.src=h;var e=y[w];return!e&&""!==e||"undefined"==n[w]||(n[w]=e),n.onload=i,n.onerror=a,n.onreadystatechange=function(e,t){"loaded"!==n.readyState&&"complete"!==n.readyState||i(0,t)},n}();y.ld<0?l.getElementsByTagName("head")[0].appendChild(e):setTimeout(function(){l.getElementsByTagName(k)[0].parentNode.appendChild(e)},y.ld||0)}try{m.cookie=l.cookie}catch(p){}function t(e){for(;e.length;)!function(t){m[t]=function(){var e=arguments;g||m.queue.push(function(){m[t].apply(m,e)})}}(e.pop())}var n="track",r="TrackPage",o="TrackEvent";t([n+"Event",n+"PageView",n+"Exception",n+"Trace",n+"DependencyData",n+"Metric",n+"PageViewPerformance","start"+r,"stop"+r,"start"+o,"stop"+o,"addTelemetryInitializer","setAuthenticatedUserContext","clearAuthenticatedUserContext","flush"]),m.SeverityLevel={Verbose:0,Information:1,Warning:2,Error:3,Critical:4};var s=(d.extensionConfig||{}).ApplicationInsightsAnalytics||{};if(!0!==d[I]&&!0!==s[I]){var c="onerror";t(["_"+c]);var u=T[c];T[c]=function(e,t,n,a,i){var r=u&&u(e,t,n,a,i);return!0!==r&&m["_"+c]({message:e,url:t,lineNumber:n,columnNumber:a,error:i}),r},d.autoExceptionInstrumented=!0}return m}(y.cfg);function a(){y.onInit&&y.onInit(n)}(T[t]=n).queue&&0===n.queue.length?(n.queue.push(a),n.trackPageView({})):a()}(window,document,{ src: "https://js.monitor.azure.com/scripts/b/ai.2.min.js", // The SDK URL Source // name: "appInsights", // Global SDK Instance name defaults to "appInsights" when not supplied // ld: 0, // Defines the load delay (in ms) before attempting to load the sdk. -1 = block page load and add to head. (default) = 0ms load after timeout, // useXhr: 1, // Use XHR instead of fetch to report failures (if available), crossOrigin: "anonymous", // When supplied this will add the provided value as the cross origin attribute on the script tag // onInit: null, // Once the application insights instance has loaded and initialized this callback function will be called with 1 argument -- the sdk instance (DO NOT ADD anything to the sdk.queue -- As they won't get called) cfg: { // Application Insights Configuration connectionString: "CONNECTION_STRING" }}); </script>

This necessitates manually encoding the connection string, which is what I'm trying to avoid. So I guess the question is: for a react app where there is no _Layout.cshtml, can the CDN injection still find a way to use the ASP.NET Core env variable for the connection string?

Thanks for the support!

-AJN

alexnesh avatar Mar 16 '23 17:03 alexnesh

@alexnesh I remember there is .env file in react apps (I might be wrong). Inside .env, if you add prefix REACT_APP_ to your env variables, for example REACT_APP_CS = connectionstring it could be used by client side. and inside html, you could use connectionString: %REACT_APP_CS%

Karlie-777 avatar Mar 16 '23 23:03 Karlie-777

This Issue will be closed in 30 days. Please remove the "Stale" label or comment to avoid closure with no action.

github-actions[bot] avatar Jan 11 '24 07:01 github-actions[bot]