Enable GA and other trackers only for production when assets are prebuilt with webpack - web

I'm shipping frontend application I want to track stats for (GA and other trackers). The app is prebuilt with webpack on CI and I'd like to have the same bundle to be shipped to both staging and prod environments.
But there is an issue with that: if I enable trackers during build time they'll be enabled on both envs.
One way I see is to add set window.enableTrackers = true flag in index.html when sending app code from server and then decide whether to install trackers or not, but it seems to be pretty ugly solution.
Is there any other way u use to do it? Checking domain name on the client maybe? Something like:
if(document.location.hostname === 'prod.example.com') {
// enable trackers
}

This would be best resolved at the time the compilation is made by configuring Webpack differently for the production and stage bundles, where you could use the CLI --define or --env to explicitly enable tracking.
If you don't resolve the inclusion of tracking during the compilation, then you will have to resort to other means. One option is to evaluate during runtime whether the GA script should be loaded in the document. Here's an example of a Rect element that will load the script only if the hostname matches a configurable regular expression:
import React, {Component, PropTypes} from 'react'
const production = new RegExp(RE_PRODUCTION_HOST)
export const ga = window.ga = window.ga || function () {
(ga.q = ga.q || []).push(arguments)
}
export class Analytics extends Component {
static propTypes = {
id: PropTypes.string.isRequired
};
componentDidMount () {
// Runtime evaluation for loading the GA script
if (!self.location.hostname.match(production)) {
return
}
const script = document.createElement('script')
script.src = 'https://www.google-analytics.com/analytics.js'
script.async = true
document.body.appendChild(script)
}
render () {
ga.l = +new Date()
ga('create', this.props.id, 'auto')
ga('send', 'pageview')
return null
}
}
Here, I can still safely define RE_PRODUCTION_HOST to be configured to production.com or even ^(www.)?production.com% and I can change it during test compilations to make assertions.
Another option is to add filters in Google Analytics that will include/exclude results depending on the hostname. This won't require you to make any further changes in your code.

Related

How does one secure api keys on sveltekit 1.0

I am using ghost, i made an integration and i would like to hide the api key from the front-end. I do not believe i can set restrictions on the ghost cms (that would also work). And i do believe so +page.js files are run on the browser also, so im a little confused on how to achieve this?
The interal sveltekit module $env/static/private (docs) is how you use secure API keys. Sveltekit will not allow you to import this module into client code so it provides an extra layer of safety. Vite automatically loads your enviroment variables from .env files and process.env on build and injects your key into your server side bundle.
import { API_KEY } from '$env/static/private';
// Use your secret
Sveltekit has 4 modules for accessing enviroment variables
$env/static/private (covered)
$env/static/public accessiable by server and client and injected at build (docs)
$env/dynamic/private provided by your runtime adapter; only includes variables with that do not start with the your public prefix which defaults to PUBLIC_ and can only be imported by server files (docs)
$env/dynamic/public provided by your runtime adapter; only includes variables with that do start with the your public prefix which defaults to PUBLIC_ (docs)
You don't need to hide the key.
Ghost Content API Docs:
These keys are safe for use in browsers and other insecure environments, as they only ever provide access to public data.
One common way to hide your third-party API key(s) from public view is to set up proxy API routes.
The general idea is to have your client (browser) query a proxy API route that you provide/host, have that proxy route query the third-party API using your credentials (API key), and pass on the results from the third-party API back to the client.
Because the query to the third-party API takes place exclusively on the back-end, your credentials are never exposed to the client (browser) and thus not visible to the public.
In your use case, you would have to create 3 dynamic endpoint routes to replicate the structure of Ghost's API:
src/routes/api/[resource]/+server.js to match /posts/, /authors/, /tags/, etc.:
const API_KEY = <your_api_key>; // preferably pulled from ENV
const GHOST_URL = `https://<your_ghost_admin_domain>/ghost/api/content`;
export function GET({ params, url }) {
const { resource } = params;
const queryString = url.searchParams.toString();
return fetch(`${GHOST_URL}/${resource}/?key=${API_KEY}${queryString ? `&${queryString}` : ''}`, {
headers: {
'Accept-Version': '5.0' // Ghost API Version setting
}
});
}
src/routes/api/[resource]/[id]/+server.js to match /posts/{id}/, /authors/{id}/, etc.:
const API_KEY = <your_api_key>; // preferably pulled from ENV
const GHOST_URL = `https://<your_ghost_admin_domain>/ghost/api/content`;
export function GET({ params, url }) {
const { resource, id } = params;
const queryString = url.searchParams.toString();
return fetch(`${GHOST_URL}/${resource}/${id}/?key=${API_KEY}${queryString ? `&${queryString}` : ''}`, {
headers: {
'Accept-Version': '5.0' // Ghost API Version setting
}
});
}
src/routes/api/[resource]/slug/[slug]/+server.js to match /posts/slug/{slug}/, /authors/slug/{slug}/, etc.:
const API_KEY = <your_api_key>; // preferably pulled from ENV
const GHOST_URL = `https://<your_ghost_admin_domain>/ghost/api/content`;
export function GET({ params, url }) {
const { resource, slug } = params;
const queryString = url.searchParams.toString();
return fetch(`${GHOST_URL}/${resource}/slug/${slug}/?key=${API_KEY}${queryString ? `&${queryString}` : ''}`, {
headers: {
'Accept-Version': '5.0' // Ghost API Version setting
}
});
}
Then all you have to do is call your proxy routes in place of your original third-party API routes in your app:
// very barebones example
<script>
let uri;
let data;
async function get() {
const res = await fetch(`/api/${uri}`);
data = await res.json();
}
</script>
<input name="uri" bind:value={uri} />
<button on:click={get}>GET</button>
{data}
Note that using proxy API routes will also have the additional benefit of sidestepping potential CORS issues.

How to set environment variable for ApolloClient that should be server side rendered for CI/CD

I have the following apolloClient
/**
* Initializes an ApolloClient instance. For configuration values refer to the following page
* https://www.apollographql.com/docs/react/api/core/ApolloClient/#the-apolloclient-constructor
*
* #returns ApolloClient
*/
const createApolloClient = (authToken: string | null) => {
const httpLinkHeaders = {
...(authToken && { Authorization: `Bearer ${authToken}` })
};
console.log('CREATING APOLLO CLIENT WITH HEADERS >>>>', httpLinkHeaders);
console.log(
'Graph Env Variable URL >>>>>',
publicRuntimeConfig.GRAPHQL_URL
);
return new ApolloClient({
name: 'client',
ssrMode: typeof window === 'undefined',
link: createHttpLink({
uri: publicRuntimeConfig.GRAPHQL_URL,
credentials: 'same-origin',
headers: httpLinkHeaders
}),
cache: new InMemoryCache()
});
};
/**
* Initializes the apollo client with data restored from the cache for pages that fetch data
* using either getStaticProps or getServerSideProps methods
*
* #param accessToken
* #param initialState
*
* #returns ApolloClient
*/
export const initializeApollo = (
accessToken: string,
initialState = null,
forceNewInstane = false
): ApolloClient<NormalizedCacheObject> => {
// Regenerate client?
if (forceNewInstane) {
apolloClient = null;
}
const _apolloClient = apolloClient || createApolloClient(accessToken);
// for pages that have Next.js data fetching methods that use Apollo Client,
// the initial state gets hydrated here
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract();
// Restore the cache using the data passed from
// getStaticProps/getServerSideProps combined with the existing cached data
_apolloClient.cache.restore({ ...existingCache, ...initialState });
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient;
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient;
};
/**
* Hook to initialize the apollo client only when state has changed.
*
* #param initialState
*
* #returns
*/
export const useApollo = (
initialState: any
): ApolloClient<NormalizedCacheObject> => {
return useMemo(() => {
if (process.browser) {
const accessToken = extractCookie(document.cookie, 'access_token');
return initializeApollo(accessToken, initialState);
}
// document is not present and we can't retrieve the token but ApolloProvider requires to pass a client
return initializeApollo(null, initialState);
}, [initialState]);
};
That is initialized in the _app.tsx file like so
const updateApolloWithNewToken = useCallback(
(accessToken: string) => {
// Initialize apollo client with new access token
setClient(
initializeApollo(accessToken, pageProps.initialApolloState, true)
);
// Show the dashboard
router.replace('/dashboard');
},
[router]
);
With the following Next Config
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
module.exports = (phase, { defaultConfig }) => {
console.log('Phase >>>>', phase);
if (phase === PHASE_DEVELOPMENT_SERVER) {
console.log('RETURNING DEVELOPMENT CONFIGURATION...');
return {
publicRuntimeConfig: {
GRAPHQL_URL: process.env.GRAPHQL_URL
}
};
}
console.log('RETURNING PRODUCTION CONFIGURATION...');
console.log('GRAPHQL_URL', process.env.GRAPHQL_URL);
return {
publicRuntimeConfig: {
GRAPHQL_URL: process.env.GRAPHQL_URL
}
};
};
This is my _app.tsx
function MyApp({ Component, pageProps }: AppProps) {
// Grab the apollo client instance with state hydration from the pageProps
const router = useRouter();
const apolloClient = useApollo(pageProps.initialApolloState);
const [client, setClient] = useState(apolloClient);
React.useEffect(() => {
// Refresh token on browser load or page regresh
handleAcquireTokenSilent();
// We also set up an interval of 5 mins to check if token needs to be refreshed
const refreshTokenInterval = setInterval(() => {
handleAcquireTokenSilent();
}, REFRESH_TOKEN_SILENTLY_INTERVAL);
return () => {
clearInterval(refreshTokenInterval);
};
}, []);
const updateApolloWithNewToken = useCallback(
(accessToken: string) => {
// Initialize apollo client with new access token
setClient(
initializeApollo(accessToken, pageProps.initialApolloState, true)
);
// Show the dashboard
router.replace('/dashboard');
},
[router]
);
return pageProps.isAuthenticated || pageProps.shouldPageHandleUnAuthorize ? (
<ApolloProvider client={client}>
<ThemeProvider theme={theme}>
<SCThemeProvider theme={theme}>
<StylesProvider injectFirst>
<Component
{...pageProps}
updateAuthToken={updateApolloWithNewToken}
/>
</StylesProvider>
</SCThemeProvider>
</ThemeProvider>
</ApolloProvider>
) : (
<UnAuthorize />
);
}
/**
* Fetches the Me query so that pages/components can grab it from the cache in the
* client.
*
* Note: This disables the ability to perform automatic static optimization, causing
* every page in the app to be server-side rendered.
*/
MyApp.getInitialProps = async (appContext: AppContext) => {
const appProps = await App.getInitialProps(appContext);
const req = appContext.ctx.req;
// Execute Me query and handle all scenarios including, unauthorized response, caching data so pages can grab
// data from the cache
return await handleMeQuery(appProps, req);
};
export default MyApp;
My problem is that when I run yarn build I get a server error generating 500 page. I know it is because when creating the Apollo Client it doesn't have access to the publicRuntimeConfig, it seems like Next is trying to build the ApolloClient when I run yarn build, I am using getInitialProps and getServerSideProps so I just want to access all the env variables on runtime not on build, because we want one build for our pipeline.
All other env variables in my app that are using publicRuntimeConfig are working, I tested by removing the env vars on build and adding them back on start and the app functioned as normal.
If there isn't a way to do this with apollo client, what would be reccomended to be able to pass different uri's as env variables on start of the app not on build for Apollo Client or alternate solution?
Thanks for any help ahead of time
So I don't know if I have explained the problem well enough.
Basically the graphql URL is different depending on the environment it is in in development, staging, and production however they are supposed to use the same build so I need to access the GRAPHQL_URL via a runtime variable, but in my current setup it is just undefined.
Generally you don't want to use publicRuntimeConfig because it adds overhead and is unnecessary for your use-case. It also disables automatic static optimization.
Traditionally, environment variables are the way to handle dynamic settings based on the environment. Next.js has three default environments – development, test, and production.
Your Next.js app will automatically grab (and merge) the correct variables depending on the environment.
.env.local
GRAPHQL_URL = localhost:8000/graphql
.env.test
GRAPHQL_URL = test.example.com/graphql
.env or .env.production
GRAPHQL_URL = production.example.com/graphql
Apollo config
new ApolloClient({
link: createHttpLink({
uri: process.env.GRAPHQL_URL,
})
});
Environment variable files
In your project root you can create files named
.env
.env.local
.env.test
.env.test.local
.env.production
I believe you can append .local to any environment to create a local only version .env.production.local - this has limited use in your case
Next.js Environment variables
All environments - .env
loaded in development, test, and production environments. This file is used for defaults across all environments. The variables in this file will be overroad if another environment variable file has a variable with the same name.
Development environment .env.local
loaded only when NODE_ENV = development (next dev)
Test environment .env.test
loaded only when NODE_ENV = test
Test environment .env.test.local
loaded only when NODE_ENV = test and local
Production environment .env.production
loaded only when NODE_ENV = production (next start)
Example production only variable
Create a .env.production and add the variable there. You can repeat the same for test and local only variables.
Notes
It's good practice to add your .env to your .gitignore so you don't accidently commit secrets to your repositories. At a minimum you should omit .env*.local files from git.
Depending on your CI/CD setup you can set your environment variables in the deployment platform like vercel, github actions, etc. This would allow you to set the test and production variables in the hosting platform and not in your code.
If you need an environment variable accessible in the browser you need to prefix the variabel with NEXT_PUBLIC_
First, there is redundant code and inefficiency. Primarily the hooks updateApolloWithNewToken and useApollo but also in the way you inject the accessToken.
I would recommend throwing the ApolloClient in it's own separate file and using it's configurable links for your use-case.
However, the real problem most likely lies in the initialization of the client and your attempt at memoizing the client.
First, try updating the following,
link: createHttpLink({
uri: publicRuntimeConfig.GRAPHQL_URL,
credentials: 'same-origin',
headers: httpLinkHeaders
}),
to
link: createHttpLink({
// ...your other stuff
uri: () => getConfig().publicRuntimeConfig.GRAPHQL_URL
})
If that doesn't work right away, I would recommend trying with the outmost minimal example.
Create a client you export, a provider and a component that uses it (without using state, useEffect or anything else). We can go from there if that still doesn't work.

Can Open Telemetry Instrument Two Express Services in the Same Node Program?

Let's say I have a NodeJS program that has two separate instances of an express server running.
const express = require('express')
const app1 = express()
app1.listen(3000, () => { //... })
//...
const app2 = express()
app2.listen(3001, () => { //... })
I've been able to instrument a program like this via open telemetry, and have my spans sent/exported successfully to Zipkin. All I needed to do is/was add code like the following to the start of my program.
const { NodeTracerProvider } = require('#opentelemetry/node');
const { ZipkinExporter } = require('#opentelemetry/exporter-zipkin');
const provider = new NodeTracerProvider({
plugins: {
express: {
enabled: true,
},
http: {
requestHook: (span, request) => {
span.setAttribute("custom request hook attribute", "request");
},
},
}
});
const options = {
url: 'http://localhost:9411/api/v2/spans',
serviceName: 'service-main'
}
const zipkinExporter = new ZipkinExporter(options);
provider.addSpanProcessor(new SimpleSpanProcessor(zipkinExporter))
provider.register();
and make sure that the express and http plugins were/are installed
npm install #opentelemetry/plugin-http #opentelemetry/plugin-express
This all works great -- except for one thing. Open Telemetry sees both my express services running as the same service-main service.
When I instrumented these services directly with Zipkin -- I would add the Zipkin middleware to each running express server
app1.use(zipkinMiddleware({tracer: tracer1}));
app2.use(zipkinMiddleware({tracer: tracer2}));
Each tracer could be instantiated with its own service name, which allowed each service to have its individual name and show up as a different service in Zipkin.
(/main, /hello, and /goobye are all service via a different express service in the above URL)
Is this sort of thing (instrumenting two services in one program) possible with Open Telemetry? Or would I need to separate these two services out into separate programs in order to have each services have an individual name? This question is less about solving a particular problem, and more about understanding the semantics of Open Telemetry.
It is possible to create two separate tracer providers. Only one of them will be the global tracer provider, which the API will use if you call API methods. You can't use the plugins in this configuration, which means you will have to manually instrument your application. If this is a use-case which is important to you, I suggest you create an issue on the github repo.
yes, you can have multiple express running in the same node process (thats how clustering works in node as well)
but you will need to have them running on different ports.;
# const express = require('express')
const app1 = express()
app1.listen(3001, () => { //... })
//...
const app2 = express()
app2.listen(3002, () => { //... })

Can a global variable set during a request leak across different requests?

If I set a global variable during processing of a request, can I leak that information into subsequent requests?
That is, the global variable is set on every request, and because nodejs is single-threaded I think I'm safe, but I'm asking here to increase confidence.
Here's my context:
I'm building a front-end service (Typescript + React) which does server side rendering, and I want to configure the application per-request.
To that end I'm using this technique:
conf.ts:
let CONFIG: any
export function setGlobal (state: any): void {
CONFIG = state
}
export function getGlobal(): any {
return CONFIG
}
server.tsx - where the Express app is configured:
const app = express()
app.get("/*", htmlEndpoint)
function htmlEndpoint (req: express.Request, res: express.Response): void {
const foo = req.headers["x-foo"] as string || ""
setGlobal({ foo })
const context = {}
const markup = renderToString(
<StaticRouter location={req.url}>
<App/>
</StaticRouter>,
)
<... respond with HTML, just boring boilerplate code ...>
}
And now somewhere deep in the React render-tree, a component can do this:
export function MyComponent() {
console.log(getGlobal().foo)
}
Technically my question is very narrow in scope: By calling setGlobal on every request, am I running a real risk of leaking the contents of that global into other requests?
But more broadly I also welcome any kind of feedback and thoughts on the design. I dislike globals but haven't found another solution, but I also recognize I'm not an expert in any of my chosen technologies so it may be you can help open my eyes to alternatives. Very happy to hear your thoughts.
You are right - since Node is single threaded only one request is processed at a time, meaning that it would most certainly not be possible for the value to leak over into another request.
I think your approach is fine, although the code might not be as readable as it could be. Would it not be simpler to pass the value as a prop instead?
To do that you could do like this:
const global = { value: null }
// To set:
global.value = 'foo'
// To read:
console.log(global.value)
To pass it as a prop, you would declare your App component like:
function App(props) {
// To access
console.log(props.global.value)
}
And in your request:
<App global={global} />
You would then need to pass the value down the react tree to whichever component needs it.

Angular +Workbox = build ChunkLoadError: Loading chunk # and Refused to execute script because its MIME

I have added Workbox to Angular in first production deploy everything works fine, but after updating a module and rebuilding angular and injecting Workbox then visiting the site i see the service worker updates to the new version and refreshes the page, but now trying to use the updated module I get errors
Refused to execute script from 'https://example.com/8-es2015.203674bf0547eff7ff27.js'
because its MIME type ('text/html') is not executable,
and strict MIME type checking is enabled.
main-es2015.45ba4a512f87eefb1b3a.js:1 ERROR Error: Uncaught (in promise): ChunkLoadError:
Loading chunk 8 failed.(error: https://example.com/8-es2015.203674bf0547eff7ff27.js)
ChunkLoadError: Loading chunk 8 failed......
I looked at the network in chrome and I see that the file 8-es2015.203674bf0547eff7ff27.js is being served from the (disk cache) unlike the rest of the files which get served by (ServiceWorker), its content is the index.html file I don't know where it came from its not even part of the new build ? chrome places it in top frame section under scripts
Whats the reason for this Error, in the angular.json I have "outputHashing": "all", I delete everything and rebuild but still this errors, its until I clear the browser cash remove the ServiceWorker and hard refresh that the error stops happening until I reload page and it returns. Do I need to delete all the cache after every update, I thought Workbox does this automatically.Do I add something like so in the sw.js
self.addEventListener('activate', event => event.waitUntil(
caches.keys().then(cacheNames => cacheNames.forEach(name => caches.delete(name)))
)
);
Am using express, so I have set the maxAge on the sw.js to 0 and even change the public route to static files to a deep route but nothing
app.use('/sw.js', express.static(path.resolve('./public/dist/static/sw.js'), {maxAge: 0}));
app.use('/', express.static(path.resolve('./public/dist/static/'), {maxAge: 86400000}));
tools: angular 8.2.4 - workbox 4.3.1
Update
Removed workbox and the app worked, am guessing its cause of their new package workbox-window or the way am trying to use it. I have placed it in module service that is loaded from app.module then the service is called from a AppComponent ngOnInit. This could be the wrong way of initializing it.
code setup:
import {Workbox} from 'workbox-window';
#Injectable()
export class WorkerService {
supportWorker: boolean;
supportPush: boolean;
constructor(#Inject(WINDOW) private window: any, private loggerService: LoggerService) {
this.supportWorker = ('serviceWorker' in navigator);
this.supportPush = (this.supportWorker && 'PushManager' in window);
}
initWorker() {
if (this.supportWorker && environment.production) {
const wb = new Workbox('sw.js');
if (wb) {
wb.addEventListener('installed', event => {
if (event.isUpdate) {
// output a toast translated message to users
this.loggerService.info('App.webWorkerUpdate', 10000);
setTimeout(() => this.window.location.reload(), 10000);
}
});
wb.addEventListener('activated', event => {
if (!event.isUpdate) {
this.loggerService.success('App.webWorkerInit', 10000);
}
});
wb.register();
}
}
}
}
This the app component, i thought it would be best to add it to main.ts after bootstrapModule.then() but I don't know how inject a service in this method
#Component({
selector: 'app-root',
template: '<route-handler></route-handler>'
})
export class AppComponent implements OnInit {
constructor(private ws: WorkerService) {
}
ngOnInit(): void {
this.ws.initWorker();
}
}
After setting up Workbox in a different way it worked, the problem effected even chrome which failed to clear all cache after each build when testing it, had to use incognito to make sure everything works.
Here is the solution thanks to Ralph Schaer article a must read. His method is not to Cache-Bust the chunks angular build generates, also he globs in all the production scripts of workbox used by the app into the build folder and finally in the index.html he calls on workbox-window to register the service-worker.

Resources