Skip to content

Commit

Permalink
Improve the types and exports (#3)
Browse files Browse the repository at this point in the history
* Improve the types and exports

This should make the package a lot easier to use in other projects.

### Added

- An index.js that exports everything from the package
- Compiled javascript in the published package
- Separate type definitions in the published package

### Changed

- The middlewares are now functions instead of objects with methods
- The attribution models are now functions instead of objects with methods
  • Loading branch information
JeroenBakker committed Jul 26, 2023
1 parent 3f04fe2 commit 2524f6d
Show file tree
Hide file tree
Showing 31 changed files with 185 additions and 160 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ coverage/

.npmrc
package-lock.json
.DS_Store
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ For easy reference, some examples of formats are kept at the bottom of this file

## [Unreleased]

## [0.2.0] - 2023-07-26

### Added

- An index.js that exports everything from the package
- Compiled javascript in the published package
- Separate type definitions in the published package

### Changed

- The middlewares are now functions instead of objects with methods
- The attribution models are now functions instead of objects with methods

## [0.1.0] - 2023-07-24

The first version!
Expand Down
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,21 @@ This is not meant to be a full replacement of an analytics tool,
which usually measures things such as sessions, landing pages, interactions, revenue and more.
This just attributes, and it is small enough (around 1kb gzipped) that it can be used alongside such tools if you want to.

## Installation

This package is on npm:

```shell
npm install @jeroen.bakker/just-attribute
```

## Usage

In its most basic form logging interactions (pageviews) is as simple as:

```javascript
import { InteractionLogger } from '@jeroen.bakker/just-attribute';

const logger = new InteractionLogger(localStorage);
logger.pageview();
```
Expand All @@ -39,12 +49,13 @@ When it is time to finalize the list of interactions (i.e. when a user "converts
run the log of interactions through one of the included attribution models and clear the log.

```javascript
import { InteractionLogger, firstInteraction } from '@jeroen.bakker/just-attribute';

const logger = new InteractionLogger(localStorage);
const firstInteraction = new FirstInteraction();

// Do whatever you want with the attribution, such as sync it to your server
// it might also be a good idea to sync the logs themselves to learn from them or to debug attribution
const attribution = firstInteraction.attribute(logger.interactionLog());
const attribution = firstInteraction(logger.interactionLog());

// Clear the log so you don't endlessly collect interactions that have already been attributed
logger.clearLog();
Expand Down Expand Up @@ -128,12 +139,12 @@ They are simply executed by the logger itself after the initial interaction has
Once all middlewares are done it is determined whether attribution has changed, and if so the interaction is logged.

A few middlewares have been provided:
* [Google Ads](src/InteractionMiddlewares/GoogleAdsMiddleware.ts)
* [Google Ads](src/InteractionMiddlewares/GoogleAds.ts)
This sets a source / medium of google / cpc for any URL containing a `gclid` parameter.
Additionally, the parameter is logged as an important parameter, meaning attribution will change if it has a different value in a new interaction.
* [Facebook Ads](src/InteractionMiddlewares/FacebookAdsMiddleware.ts)
* [Facebook Ads](src/InteractionMiddlewares/FacebookAds.ts)
Similar to the above middleware, it sets a source / medium of facebook / cpc for any URL containing a `fbclid` parameter.
* [`ref` transformer](src/InteractionMiddlewares/RefMiddleware.ts)
* [`ref` transformer](src/InteractionMiddlewares/Ref.ts)
If a URL conains a `ref` parameter this will set its value as the source and sets the medium to referral.

Please see the source of these middlewares for further details on their behavior.
Expand Down Expand Up @@ -162,6 +173,7 @@ Planned:
- Add out of the box implementation for running attribution models in BigQuery using javascript UDFs
- Describe how to contribute
- Add a code style linter/fixer to make contributing easier
- Set up GitHub action to publish to npm

Undecided:
- Whether to log the page URL as part of the interaction, this would allow users to get information about landing pages and how they perform.
Expand Down
13 changes: 0 additions & 13 deletions debug.ts

This file was deleted.

4 changes: 0 additions & 4 deletions index.ts

This file was deleted.

4 changes: 3 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export default {

// An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: [
'src/**/*.ts'
'src/**/*.ts',
'!src/types.ts',
'!src/index.ts',
],

// The directory where Jest should output its coverage files
Expand Down
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
{
"name": "@jeroen.bakker/just-attribute",
"version": "0.1.0",
"version": "0.2.0",
"description": "Realtime privacy-conscious marketing attribution for the web",
"author": "Jeroen Bakker",
"license": "MIT",
"scripts": {
"debug": "esbuild debug.ts --bundle --minify --outfile=./dist/just-attribute-debug.js",
"build": "esbuild index.ts --bundle --minify --outfile=./dist/just-attribute.js",
"test": "jest"
"build": "esbuild src/*.ts src/*/*.ts --outdir=dist --bundle --format=esm --minify",
"tsc": "tsc",
"test": "jest",
"prepublishOnly": "npm run build && npm run tsc"
},
"main": "src/InteractionLogger.ts",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"dependencies": {},
"devDependencies": {
"@jest/globals": "^29.6.1",
Expand All @@ -21,7 +24,7 @@
},
"files": [
"src",
"types.ts"
"dist"
],
"repository": {
"type": "git",
Expand Down
20 changes: 10 additions & 10 deletions src/AttributionModels/FirstInteraction.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { AttributionModel, Interaction } from '../../types';
import { AttributionModel, Interaction } from '../types';

/**
* This implements the "first interaction" attribution model
* which simply returns the first interaction
*
* Since only one interaction is returned, it is not weighted
*/
export default class FirstInteraction implements AttributionModel {
public attribute(interactions: Interaction[]): Interaction {
if (interactions.length === 0) {
return null;
}
const firstInteraction: AttributionModel = (interactions: Interaction[]): Interaction => {
if (interactions.length === 0) {
return null;
}

const filteredInteractions = interactions.filter((interaction) => !interaction.excluded);
const filteredInteractions = interactions.filter((interaction) => !interaction.excluded);

// If all we had were excluded interactions we return the first one as it's better than nothing
return filteredInteractions.shift() || interactions.shift();
}
// If all we had were excluded interactions we return the first one as it's better than nothing
return filteredInteractions.shift() || interactions.shift();
}

export default firstInteraction;
14 changes: 7 additions & 7 deletions src/AttributionModels/LastInteraction.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { AttributionModel, Interaction } from '../../types';
import { AttributionModel, Interaction } from '../types';

/**
* This implements the "last interaction" attribution model
* which simply returns the last interaction
*
* Since only one interaction is returned, it is not weighted
*/
export default class LastInteraction implements AttributionModel {
public attribute(interactions: Interaction[]): Interaction {
const includedInteractions = interactions.filter((interaction) => !interaction.excluded);
const lastInteraction: AttributionModel = (interactions: Interaction[]): Interaction => {
const includedInteractions = interactions.filter((interaction) => !interaction.excluded);

// Interactions are logged in order of occurrence, so we simply need to return the last one
return includedInteractions.pop() ?? null;
}
// Interactions are logged in order of occurrence, so we simply need to return the last one
return includedInteractions.pop() ?? null;
}

export default lastInteraction;
30 changes: 15 additions & 15 deletions src/AttributionModels/LastNonDirectInteraction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AttributionModel, Interaction } from '../../types';
import { AttributionModel, Interaction } from '../types';

/**
* This implements the "last non-direct interaction" attribution model
Expand All @@ -7,20 +7,20 @@ import { AttributionModel, Interaction } from '../../types';
*
* Since only one interaction is returned, it is not weighted
*/
export default class LastNonDirectInteraction implements AttributionModel {
public attribute(interactions: Interaction[]): Interaction {
if (interactions.length === 0) {
return null;
}
const lastNonDirectInteraction: AttributionModel = (interactions: Interaction[]): Interaction => {
if (interactions.length === 0) {
return null;
}

const nonExcludedInteractions = interactions.filter((interaction) => !interaction.excluded);
const nonExcludedNonDirectInteractions = nonExcludedInteractions.filter((interaction) => !interaction.direct);
const nonExcludedInteractions = interactions.filter((interaction) => !interaction.excluded);
const nonExcludedNonDirectInteractions = nonExcludedInteractions.filter((interaction) => !interaction.direct);

// First, we attempt to return the last non-excluded non-direct interaction
// Then if all we had were excluded and/or direct interactions we attempt to return the last non-excluded direct interaction
// Then if all we had were excluded interactions we return the last one as it's better than nothing
return nonExcludedNonDirectInteractions.pop()
|| nonExcludedInteractions.pop()
|| interactions.pop();
}
// First, we attempt to return the last non-excluded non-direct interaction
// Then if all we had were excluded and/or direct interactions we attempt to return the last non-excluded direct interaction
// Then if all we had were excluded interactions we return the last one as it's better than nothing
return nonExcludedNonDirectInteractions.pop()
|| nonExcludedInteractions.pop()
|| interactions.pop();
}

export default lastNonDirectInteraction;
36 changes: 18 additions & 18 deletions src/AttributionModels/Linear.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { AttributionModel, Interaction, WeightedInteraction } from '../../types';
import { AttributionModel, Interaction, WeightedInteraction } from '../types';

/**
* This implements the "linear" attribution model
* which equally distributes the attribution over all interactions
*/
export default class Linear implements AttributionModel {
public attribute(interactions: Interaction[]): WeightedInteraction[] {
if (interactions.length === 0) {
return [];
}

let includedInteractions = interactions.filter((interaction) => !interaction.excluded)
const linear: AttributionModel = (interactions: Interaction[]): WeightedInteraction[] => {
if (interactions.length === 0) {
return [];
}

// If all our interactions are excluded, ignore the exclusions anyway
if (includedInteractions.length === 0) {
includedInteractions = interactions;
}
let includedInteractions = interactions.filter((interaction) => !interaction.excluded)

return includedInteractions.map((interaction) => {
return {
...interaction,
weight: 1 / includedInteractions.length,
}
});
// If all our interactions are excluded, ignore the exclusions anyway
if (includedInteractions.length === 0) {
includedInteractions = interactions;
}

return includedInteractions.map((interaction) => {
return {
...interaction,
weight: 1 / includedInteractions.length,
}
});
}

export default linear;
68 changes: 33 additions & 35 deletions src/AttributionModels/PositionBased.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AttributionModel, Interaction, WeightedInteraction } from '../../types';
import { AttributionModel, Interaction, WeightedInteraction } from '../types';

/**
* This implements the "position based" attribution model
Expand All @@ -7,46 +7,44 @@ import { AttributionModel, Interaction, WeightedInteraction } from '../../types'
* If there are 2 interactions the first and last interactions will both get 50% of the attribution,
* and if there is only 1 interaction it will of course receive 100% of the attribution.
*/
export default class PositionBased implements AttributionModel {


public attribute(interactions: Interaction[]): WeightedInteraction[] {
if (interactions.length === 0) {
return [];
}

let remainingInteractions = interactions.filter((interaction) => ! interaction.excluded);
const positionBased: AttributionModel = (interactions: Interaction[]): WeightedInteraction[] => {
if (interactions.length === 0) {
return [];
}

// If all interactions were excluded, ignore the exclusions
if (remainingInteractions.length === 0) {
remainingInteractions = interactions;
}
let remainingInteractions = interactions.filter((interaction) => !interaction.excluded);

const firstInteraction = remainingInteractions.shift();
const lastInteraction = remainingInteractions.pop();
// If all interactions were excluded, ignore the exclusions
if (remainingInteractions.length === 0) {
remainingInteractions = interactions;
}

// If there is only 1 interaction, attribute 100% to it
if (! lastInteraction) {
return [{...firstInteraction, weight: 1}];
}
const firstInteraction = remainingInteractions.shift();
const lastInteraction = remainingInteractions.pop();

// If there are only two interactions, attribute 50% to both
if (remainingInteractions.length === 0) {
return [
{...firstInteraction, weight: 0.5},
{...lastInteraction, weight: 0.5},
];
}
// If there is only 1 interaction, attribute 100% to it
if (!lastInteraction) {
return [{...firstInteraction, weight: 1}];
}

// If there are only two interactions, attribute 50% to both
if (remainingInteractions.length === 0) {
return [
{...firstInteraction, weight: 0.4,},
...remainingInteractions.map((interaction): WeightedInteraction => {
return {
...interaction,
weight: 0.2 / remainingInteractions.length,
};
}),
{...lastInteraction, weight: 0.4},
{...firstInteraction, weight: 0.5},
{...lastInteraction, weight: 0.5},
];
}

return [
{...firstInteraction, weight: 0.4,},
...remainingInteractions.map((interaction): WeightedInteraction => {
return {
...interaction,
weight: 0.2 / remainingInteractions.length,
};
}),
{...lastInteraction, weight: 0.4},
];
}

export default positionBased;
2 changes: 1 addition & 1 deletion src/InteractionLogger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InteractionMiddleware, Interaction } from '../types';
import { InteractionMiddleware, Interaction } from './types';

export default class InteractionLogger {
private static readonly logStorageKey = 'ja_interaction_log';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { InteractionMiddleware, Interaction } from '../../types';
import { InteractionMiddleware, Interaction } from '../types';

export const FacebookAdsMiddleware: InteractionMiddleware = (currentInteraction: Interaction): Interaction => {
const facebookAds: InteractionMiddleware = (currentInteraction: Interaction): Interaction => {
// If it is already attributed to something just return that
if (currentInteraction.source && currentInteraction.medium) {
return currentInteraction;
Expand All @@ -24,3 +24,5 @@ export const FacebookAdsMiddleware: InteractionMiddleware = (currentInteraction:

return interaction;
}

export default facebookAds;
Loading

0 comments on commit 2524f6d

Please sign in to comment.