Research
Security News
Quasar RAT Disguised as an npm Package for Detecting Vulnerabilities in Ethereum Smart Contracts
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
@aperture.io/analytics
Advanced tools
Declarative and compartmentalized library for React analytics, heavily inspired by the New York Times' blog post on declarative React tracking.
Let's start with a top-level <App />
component:
export const App = () => {
const { Provider } = useAnalytics(
// Event data shared by all components within our app
{ app: 'MyApp' },
// Function used to handle triggered events
// This is where you would integrate with Amplitude, Segment, etc.
{ dispatch: (eventData) => console.log('dispatch', eventData) },
);
return (
// <Provider /> passes App event data down to its children
<Provider>
<HomePage />
</Provider>
);
};
Here, we are doing three things:
<App />
and all of it childrendispatch
function that will called whenever an tracking event is triggered by one of the components<Provider />
to pass event data down to <App />
's childrenNext, let's add a <HomePage />
component:
export const HomePage = () => {
const { Provider } = useAnalytics(
// Event data shared by all components within HomePage
{ page: 'HomePage' },
// Trigger an event when the component is mounted
{ dispatchOnMount: true },
);
return (
// <Provider /> passes HomePage event data down to its children
<Provider>
<NewsletterForm />
</Provider>
);
};
In this component, we are:
<HomePage />
, to be shared with all of its childrenuseAnalytics
to dispatch an event as soon as the component is mounted. This is handy for tracking things like page views<Provider />
to pass the event data down to <HomePage />
's children.💡 Note that the
<Provider />
is returned from the localuseAnalytics
hook call. This allows<Provider />
to merge any parent event data with the local<HomePage />
event data, before passing the combined data down to<HomePage />
's children.
Finally, let's add the <NewsletterForm />
component, where our user interaction tracking will take place:
export const NewsletterForm = ({ handleSubmit }) => {
const { trackEvent } = useAnalytics(
// Event data shared by all components within NewsletterForm
{ module: 'NewsletterForm' },
);
return (
<Form
onSubmit={(event) => {
handleSubmit(event);
// Event data specific to form submission
trackEvent({ action: 'submit-form' });
}}
>
<FormLabel>Sign up for our newsletter!</FormLabel>
<Input
placeholder="Email address"
onFocus={() => {
// Event data specific to focusing the email input
trackEvent({ action: 'email-input-focused' });
}}
/>
<Button type="submit">Sign up!</Button>
</Form>
);
};
In this component we:
<NewsletterForm />
<Provider />
in this component, since it has no children that implement their own trackingWe already saw how you can define shared event data when you call useAnalytics
before. Here we are also passing event-specific data to trackEvent
when handling specific user interactions. This allows you to minimize how much data you have to pass to each trackEvent
call, without sacrificing flexibility.
Now that we have all of our components set up, let's walk through exactly what happens when a user interacts with our app. There are a total of three events that could be dispatched, so let's walk through them, one by one.
First, when <HomePage />
is mounted, an event is dispatched immediately because we set dispatchOnMount: true
. <HomePage />
is a child of <App />
, so its event data will be merged with its parent. This is what the combined data will look like:
{
// <App /> event data
app: 'MyApp',
// <HomePage /> event data
page: 'HomePage',
}
The next event is dispatched when a user focuses an input in the <NewsletterForm />
component. <NewsletterForm />
has two parents, <HomePage />
and <App />
, and we also passed some event-specific data when we called trackEvent
. Here is how this data would be merged:
{
// <App /> event data
app: 'MyApp',
// <HomePage /> event data
page: 'HomePage',
// <NewsletterForm /> shared data
module: 'NewsletterForm',
// Event-specific data passed to `trackEvent` directly
action: 'email-input-focused',
}
The last event is dispatched when a user submits the <NewsletterForm />
. It's very similar to the input focus event above, but has different event-specific data. Here it is merged:
{
// <App /> event data
app: 'MyApp',
// <HomePage /> event data
page: 'HomePage',
// <NewsletterForm /> shared data
module: 'NewsletterForm',
// Event-specific data passed to `trackEvent` directly
action: 'submit-form',
}
If you are writing a component within a larger application, it is sometimes useful to override the parent dispatch
function in your local component. Here are some possible cases where this can be useful:
dispatch
function (think "event data middleware")To see how we can do this, let's go back to our example app above, and override the dispatch
for the <HomePage />
component:
export const HomePage = () => {
const { Provider } = useAnalytics(
{ page: 'HomePage' },
{
dispatch: (eventData, parentDispatch) => {
// Disable tracking in development
if (process.env.NODE_ENV === 'development') return;
// Remove some event data
const filteredData = omit(eventData, ['debug']);
// Add some event data
const newData = { ...filteredEventData, url: window.location.href };
// Validate the event data format
if (newData.fullName) throw Error("Don't track personal user data!");
// Send data to a different analytics service
Segment.track(eventData);
// Finally, pass the modified data to the parent dispatch function
parentDispatch(newData);
},
dispatchOnMount: true,
},
);
return (
<Provider>
<NewsletterForm />
</Provider>
);
};
We are doing a lot of things: piping data to a different provider, removing, adding, and validating data on the fly, and eventually passing the modified data up to the parent dispatch
function. And the best part, since the dispatch
function override happens locally in <HomePage />
, the rest of the application is unaffected. As you can see, this approach can be very powerful!
Our dispatch
function in the last example got rather complicated. It's not exactly "doing one thing, and doing it well". We also don't have a way to easily re-use our code in a different component
The first thing we could do is break up the dispatch
function into separate helper functions:
// Remove some event data
const filterFields = (data, fieldsToOmit) => {
const nextData = omit(data, fieldsToOmit);
return nextData;
};
// Add some event data
const trackUrl = (data) => {
const nextData = { ...data, url: window.location.href };
return nextData;
};
// Validate the event data format
const validateEventData = (data) => {
if (data.fullName) throw Error("Don't track personal user data!");
return data;
};
// Send data to a different analytics service
const pushToSegment = (data) => {
Segment.track(data);
return data;
};
const dispatch = (eventData, parentDispatch) => {
// Disable tracking in development
if (process.env.NODE_ENV === 'development') return;
let data = eventData;
data = filterFields(data, ['debug']);
data = trackUrl(data);
data = validateEventData(data);
data = pushToSegment(data);
// Finally, pass the modified data to the parent dispatch function
parentDispatch(data);
};
That's certainly looking better, and we most of our code is not reusable! There are, however, still two issues remaining:
data
argument, but that's hardly a good way to write modern JavaScript. It would be nice if we had a way to get rid of all of this boilerplate 🤔development
. We could have added an isDisabled
function, but we'd still need an if
statement in our dispatch
function. To put it another way, our helpers can't easily "short-circuit" the dispatch
function. This seems like something we might need, especially if we have a lot of reusable analytics helpers in a large codebase! 😟This is where middleware comes in! A middleware function typically takes some data (or state), performs some action, and then, if everything looks good, calls the next middleware function. You've probably encountered middleware in libraries like express
or redux
before. When used effectively, middleware allows developers to implement complex behaviors using simple, reusable, and composable functions, and helps cut down on boilerplate code.
So, what does middleware look like in this library? Well, you've already seen it. Take another look at a dispatch
function:
const dispatch = (eventData, parentDispatch) => {
// Possibly short-circuit...
if (process.env.NODE_ENV === 'development') return;
// Do something...
const nextData = filterFields(eventData, ['debug']);
// Everything looks good, pass the data along to the next dispatch function!
parentDispatch(nextData);
};
That... sounds a lot like the middleware function we described earlier! It's even more obvious if you rename a few fields:
const middleware = (data, next) => {
// Possibly short-circuit...
if (process.env.NODE_ENV === 'development') return;
// Do something...
const nextData = filterFields(data, ['debug']);
// Everything looks good, pass the data along to the next middleware function!
next(nextData);
};
Now, let's rewrite our helper functions from earlier:
// Remove some event data
const filterFields = (fieldsToOmit) => (data, next) => {
const nextData = omit(data, fieldsToOmit);
next(nextData);
};
// Add some event data
const trackUrl = (data, next) => {
const nextData = { ...data, url: window.location.href };
next(nextData);
};
// Validate the event data format
const validateEventData = (data, next) => {
if (data.fullName) throw Error("Don't track personal user data!");
next(data);
};
// Send data to a different analytics service
const pushToSegment = (data, next) => {
Segment.track(data);
next(data);
};
Very similar so far! This just leaves the logic that disables tracking in a development
environment. What we are really trying to do here is to not call any middleware after the environment check. We know that calling next
from one middleware function is how we tell the next middleware function to run, so all we have to do is not call next
, and our middleware execution chain stops:
const disableInDevelopment = (data, next) => {
// In development, return without calling `next` to prevent additional
// middleware from running
if (process.env.NODE_ENV === 'development') return;
// For other environments, call `next` to keep the middleware execution going
next();
};
The last thing we need is to somehow combine all our middleware functions into a single dispatch
function that can be passed to the useAnalytics
good. You can do this using the applyMiddleware
helper function provided by this library:
import { applyMiddleware } from '@aperture.io/analytics';
// Merge our middleware functions into a single dispatch function
const dispatch = applyMiddleware([
disableInDevelopment,
filterFields(['debug']),
trackUrl,
validateEventData,
pushToSegment,
]);
That's much cleaner! ✨
The applyMiddleware
function takes an array of middleware functions, Middleware[]
, and returns a single, merged Middleware
function. The dispatch
function is also of type Middleware
. This means that you can nest calls to applyMiddleware
, and any previously-defined dispatch
function can be passed as middleware to applyMiddleware
:
const someDispatchFunction = // ...
const dispatch = applyMiddleware([
someDispatchFunction,
applyMiddleware([someMiddleware, anotherMiddleware]),
oneMoreMiddleware,
]);
Despite our best efforts, our code will inevitably throw unexpected runtime errors. In this library's case, the most common errors we might run into are:
dispatch
functionsWhen an error is thrown, the useAnalytics
hook returns an additional error
object:
const { error } = useAnalytics(
{ some: 'data' },
{
dispatch: () => {
throw Error('Uh oh');
},
},
);
Given the highly-nested nature of this library, it is important to note that errors are handled in the local call to useAnalytics
, and do not bubble up to parents.
In our grocery list example from earlier, events were triggered from the <GroceryCheckbox />
component, so that is where the error
object will be available:
const GroceryCheckbox = () => {
// 👇 Call to trackEvent happens in this component, so the error is caught here
const { trackEvent, error } = useAnalytics();
const [checked, setChecked] = useState(false);
return (
<input
type="checkbox"
checked={checked}
onChange={() => {
trackEvent({
action: 'done-button-toggle',
bought: !checked,
});
setChecked(!checked);
}}
/>
);
};
To avoid making assumption about the desired merging behavior, this library merges event data shallowly by default. Deeply merging event data is a large application with many levels of tracked parent and child components can get complicated quickly, so it is generally discouraged.
If you do require more complex data shapes, you have the option to override the default merge using the mergeData
config option:
import { merge } from 'lodash';
const { error } = useAnalytics(
{ nested: { some: 'data' } },
{ mergeData: (parentData, localData) => merge({}, parentData, localData) },
);
💡 Note that you can pass different mergeData
functions to each call to useAnalytics
, so each nested tracked component has full control over how its local event data merging is handled.
And that's the library! Happy tracking! 🚀🚀🚀
// Types
type Dispatch = (data: Object) => void;
type Middleware = (data: Object, next: Dispatch) => void;
type UseAnalytics = (
// Shared analytics data
eventData: Object,
// Configuration object
options: {
// Dispatch event handler
dispatch: Middleware;
// Whether to dispatch an event on component mount
dispatchOnMount: true;
},
) => {
// Localized analytics provider
Provider: React.Provider<Object>;
// trackEvent function
trackEvent: (data: Object) => void;
};
// Exports
export const applyMiddleware: (fn: Middleware[]) => Middleware;
export const useAnalytics: UseAnalytics;
pnpm run build # Build library bundle
pnpm run start # Start dev server
pnpm run tsc # Start Typescript compiler
pnpm run tsc:watch # Start Typescript compiler (watch mode)
pnpm run test # Run Jest tests
pnpm run test:watch # Run Jest tests (watch mode)
pnpm run lint # Check for ESLint issues
pnpm run lint:watch # Check for ESLint issues (watch mode)
/dist # Build artifacts destination folder (compiles TS output)
/source # Library source files
/index.ts # Library entry point
/.eslint.json # Local ESLint configuration
/babel.config.js # Local Babel configuration
/jest.config.json # Local Jest configuration
/jsconfig.json # JS config, primarily used by code editors to resolve path aliases
/tsconfig.json # TS configuration
FAQs
Declarative React analytics library
The npm package @aperture.io/analytics receives a total of 8 weekly downloads. As such, @aperture.io/analytics popularity was classified as not popular.
We found that @aperture.io/analytics demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.