@posthog/plugin-unduplicates
Advanced tools
+14
| module.exports = { | ||
| parser: '@typescript-eslint/parser', | ||
| plugins: ['@typescript-eslint', 'simple-import-sort'], | ||
| extends: ['plugin:@typescript-eslint/recommended', 'prettier'], | ||
| ignorePatterns: ['bin', 'dist', 'node_modules'], | ||
| rules: { | ||
| 'simple-import-sort/imports': 'error', | ||
| 'simple-import-sort/exports': 'error', | ||
| '@typescript-eslint/no-non-null-assertion': 'off', | ||
| '@typescript-eslint/no-var-requires': 'off', | ||
| '@typescript-eslint/ban-ts-comment': 'off', | ||
| curly: 'error', | ||
| }, | ||
| } |
| name: CI | ||
| on: | ||
| - pull_request | ||
| jobs: | ||
| lint: | ||
| name: Code formatting & linting | ||
| runs-on: ubuntu-20.04 | ||
| steps: | ||
| - uses: actions/checkout@v1 | ||
| - name: Set up Node 16 | ||
| uses: actions/setup-node@v1 | ||
| with: | ||
| node-version: 16 | ||
| - uses: actions/cache@v2 | ||
| id: node-modules-cache | ||
| with: | ||
| path: | | ||
| node_modules | ||
| key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }} | ||
| restore-keys: | | ||
| ${{ runner.os }}-node-modules | ||
| - name: Install dependencies | ||
| if: steps.node-modules-cache.outputs.cache-hit != 'true' | ||
| run: yarn install --frozen-lockfile | ||
| - name: Check formatting with Prettier | ||
| run: yarn format:check | ||
| - name: Lint with ESLint | ||
| run: yarn lint | ||
| - name: Check Typescript | ||
| run: | | ||
| yarn typescript:check | ||
| test: | ||
| name: Test | ||
| runs-on: ubuntu-20.04 | ||
| steps: | ||
| - uses: actions/checkout@v1 | ||
| - name: Set up Node 16 | ||
| uses: actions/setup-node@v1 | ||
| with: | ||
| node-version: 16 | ||
| - uses: actions/cache@v2 | ||
| id: node-modules-cache | ||
| with: | ||
| path: | | ||
| node_modules | ||
| key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }} | ||
| restore-keys: | | ||
| ${{ runner.os }}-node-modules | ||
| - name: Install dependencies | ||
| if: steps.node-modules-cache.outputs.cache-hit != 'true' | ||
| run: yarn install --frozen-lockfile | ||
| - name: Run test | ||
| run: | | ||
| yarn test |
| { | ||
| "trailingComma": "es5", | ||
| "tabWidth": 4, | ||
| "semi": false, | ||
| "singleQuote": true, | ||
| "printWidth": 120 | ||
| } |
| { | ||
| "editor.formatOnSave": true | ||
| } |
| releases: | ||
| "@posthog/plugin-unduplicates": patch |
+16
| /* eslint-disable no-var */ | ||
| interface ApiMethodOptions { | ||
| data?: Record<string, any> // any data to send with the request, GET and DELETE will set these as URL params | ||
| host?: string // posthog host, defaults to https://app.posthog.com | ||
| projectApiKey?: string // specifies the project to interact with | ||
| personalApiKey?: string // authenticates the user | ||
| } | ||
| interface APIInterface { | ||
| get: (path: string, options?: ApiMethodOptions) => Promise<Record<string, any>> | ||
| } | ||
| declare namespace posthog { | ||
| var api: APIInterface | ||
| } |
+127
| import { Plugin, PluginMeta } from '@posthog/plugin-scaffold' | ||
| // @ts-ignore | ||
| import { createPageview, resetMeta } from '@posthog/plugin-scaffold/test/utils' | ||
| import { createHash } from 'crypto' | ||
| import given from 'given2' | ||
| import * as unduplicatesPlugin from '.' | ||
| const { processEvent } = unduplicatesPlugin as Required<Plugin> | ||
| given('getResponse', () => ({ results: [], next: null })) | ||
| global.posthog = { | ||
| api: { | ||
| get: jest.fn(() => | ||
| Promise.resolve({ | ||
| json: () => Promise.resolve(given.getResponse), | ||
| }) | ||
| ), | ||
| }, | ||
| } | ||
| const defaultMeta = { | ||
| config: { | ||
| dedupMode: 'Event and Timestamp', | ||
| }, | ||
| } | ||
| describe('`Event and Timestamp` mode', () => { | ||
| test('event UUID is properly generated', async () => { | ||
| const meta = resetMeta() as PluginMeta<Plugin> | ||
| const event = await processEvent({ ...createPageview(), timestamp: '2020-02-02T23:59:59.999999Z' }, meta) | ||
| expect(event?.uuid).toEqual('1b2b7e1a-c059-5116-a6d2-eb1c1dd793bc') | ||
| }) | ||
| test('same key properties produces the same UUID', async () => { | ||
| const meta = resetMeta() as PluginMeta<Plugin> | ||
| const event1 = await processEvent( | ||
| { ...createPageview(), event: 'myPageview', timestamp: '2020-05-02T20:59:59.999999Z', ignoreMe: 'yes' }, | ||
| meta | ||
| ) | ||
| const event2 = await processEvent( | ||
| { | ||
| ...createPageview(), | ||
| event: 'myPageview', | ||
| timestamp: '2020-05-02T20:59:59.999999Z', | ||
| differentProperty: 'indeed', | ||
| }, | ||
| meta | ||
| ) | ||
| expect(event1?.uuid).toBeTruthy() | ||
| expect(event1?.uuid).toEqual(event2?.uuid) | ||
| }) | ||
| test('different key properties produces a different UUID', async () => { | ||
| const meta = resetMeta() as PluginMeta<Plugin> | ||
| const event1 = await processEvent({ ...createPageview(), timestamp: '2020-05-02T20:59:59.999999Z' }, meta) | ||
| const event2 = await processEvent( | ||
| { | ||
| ...createPageview(), | ||
| timestamp: '2020-05-02T20:59:59.999888Z', // note milliseconds are different | ||
| }, | ||
| meta | ||
| ) | ||
| expect(event1?.uuid).toBeTruthy() | ||
| expect(event1?.uuid).not.toEqual(event2?.uuid) | ||
| }) | ||
| }) | ||
| describe('`All Properties` mode', () => { | ||
| test('event UUID is properly generated (all props)', async () => { | ||
| const meta = resetMeta({ | ||
| config: { ...defaultMeta.config, dedupMode: 'All Properties' }, | ||
| }) as PluginMeta<Plugin> | ||
| const event = await processEvent({ ...createPageview(), timestamp: '2020-02-02T23:59:59.999999Z' }, meta) | ||
| expect(event?.uuid).toEqual('5a4e6d35-a9e4-50e2-9d97-4f7cc04e8b30') | ||
| }) | ||
| test('same key properties produces the same UUID (all props)', async () => { | ||
| const meta = resetMeta({ | ||
| config: { ...defaultMeta.config, dedupMode: 'All Properties' }, | ||
| }) as PluginMeta<Plugin> | ||
| const event1 = await processEvent( | ||
| { | ||
| ...createPageview(), | ||
| event: 'myPageview', | ||
| timestamp: '2020-05-02T20:59:59.999999Z', | ||
| properties: { | ||
| ...createPageview().properties, | ||
| customProp1: true, | ||
| customProp2: 'lgtm!', | ||
| }, | ||
| }, | ||
| meta | ||
| ) | ||
| const event2 = await processEvent( | ||
| { | ||
| ...createPageview(), | ||
| event: 'myPageview', | ||
| timestamp: '2020-05-02T20:59:59.999999Z', | ||
| properties: { | ||
| ...createPageview().properties, | ||
| customProp1: true, | ||
| customProp2: 'lgtm!', | ||
| }, | ||
| }, | ||
| meta | ||
| ) | ||
| expect(event1?.uuid).toBeTruthy() | ||
| expect(event1?.uuid).toEqual(event2?.uuid) | ||
| }) | ||
| test('different properties produce a different UUID (all props)', async () => { | ||
| const meta = resetMeta({ | ||
| config: { ...defaultMeta.config, dedupMode: 'All Properties' }, | ||
| }) as PluginMeta<Plugin> | ||
| const event1 = await processEvent( | ||
| { ...createPageview(), timestamp: '2020-05-02T20:59:59.999999Z', properties: { customProp: '2' } }, | ||
| meta | ||
| ) | ||
| const event2 = await processEvent( | ||
| { | ||
| ...createPageview(), | ||
| timestamp: '2020-05-02T20:59:59.999999Z', | ||
| properties: { customProp: '1' }, | ||
| }, | ||
| meta | ||
| ) | ||
| expect(event1?.uuid).toBeTruthy() | ||
| expect(event1?.uuid).not.toEqual(event2?.uuid) | ||
| }) | ||
| }) |
+81
| import { Plugin, PluginEvent } from '@posthog/plugin-scaffold' | ||
| import { createHash, randomUUID } from 'crypto' | ||
| // From UUID Namespace RFC (https://datatracker.ietf.org/doc/html/rfc4122) | ||
| const NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8' | ||
| interface UnduplicatesPluginInterface { | ||
| config: { | ||
| dedupMode: 'Event and Timestamp' | 'All Properties' | ||
| } | ||
| } | ||
| const stringifyEvent = (event: PluginEvent): string => { | ||
| return `(${randomUUID().toString()}; project #${event.team_id}). Event "${event.event}" @ ${ | ||
| event.timestamp | ||
| } for user ${event.distinct_id}.` | ||
| } | ||
| const byteToHex: string[] = [] | ||
| for (let i = 0; i < 256; ++i) { | ||
| byteToHex.push((i + 0x100).toString(16).slice(1)) | ||
| } | ||
| function stringifyUUID(arr: Buffer) { | ||
| // Forked from https://github.com/uuidjs/uuid (MIT) | ||
| // Copyright (c) 2010-2020 Robert Kieffer and other contributors | ||
| return ( | ||
| byteToHex[arr[0]] + | ||
| byteToHex[arr[1]] + | ||
| byteToHex[arr[2]] + | ||
| byteToHex[arr[3]] + | ||
| '-' + | ||
| byteToHex[arr[4]] + | ||
| byteToHex[arr[5]] + | ||
| '-' + | ||
| byteToHex[arr[6]] + | ||
| byteToHex[arr[7]] + | ||
| '-' + | ||
| byteToHex[arr[8]] + | ||
| byteToHex[arr[9]] + | ||
| '-' + | ||
| byteToHex[arr[10]] + | ||
| byteToHex[arr[11]] + | ||
| byteToHex[arr[12]] + | ||
| byteToHex[arr[13]] + | ||
| byteToHex[arr[14]] + | ||
| byteToHex[arr[15]] | ||
| ).toLowerCase() | ||
| } | ||
| const plugin: Plugin<UnduplicatesPluginInterface> = { | ||
| processEvent: async (event, { config }) => { | ||
| const stringifiedEvent = stringifyEvent(event) | ||
| if (!event.timestamp) { | ||
| console.info( | ||
| 'Received event without a timestamp, the event will not be processed because deduping will not work.' | ||
| ) | ||
| return event | ||
| } | ||
| // Create a hash of the relevant properties of the event | ||
| const stringifiedProps = config.dedupMode === 'All Properties' ? `_${JSON.stringify(event.properties)}` : '' | ||
| const hash = createHash('sha1') | ||
| const eventKeyBuffer = hash | ||
| .update( | ||
| `${NAMESPACE_OID}_${event.team_id}_${event.distinct_id}_${event.event}_${event.timestamp}${stringifiedProps}` | ||
| ) | ||
| .digest() | ||
| // Convert to UUID v5 spec | ||
| eventKeyBuffer[6] = (eventKeyBuffer[6] & 0x0f) | 0x50 | ||
| eventKeyBuffer[8] = (eventKeyBuffer[8] & 0x3f) | 0x80 | ||
| event.uuid = stringifyUUID(eventKeyBuffer) | ||
| return event | ||
| }, | ||
| } | ||
| module.exports = plugin |
| const { pathsToModuleNameMapper } = require('ts-jest/utils') | ||
| const { compilerOptions } = require('./tsconfig') | ||
| const moduleNameMapper = undefined | ||
| if (compilerOptions.paths) { | ||
| moduleNameMapper = pathsToModuleNameMapper(compilerOptions.paths, { prefix: 'src/' }) | ||
| } | ||
| module.exports = { | ||
| preset: 'ts-jest', | ||
| testEnvironment: 'node', | ||
| moduleNameMapper, | ||
| setupFilesAfterEnv: ['given2/setup'], | ||
| } |
| { | ||
| "extends": "../../../tsconfig.json" | ||
| } |
+2
-2
| MIT License | ||
| Copyright (c) 2022 PostHog, Inc. | ||
| Copyright (c) 2022 Paolo D'Amico | ||
@@ -21,2 +21,2 @@ Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. | ||
| SOFTWARE. |
+1
-1
| { | ||
| "name": "@posthog/plugin-unduplicates", | ||
| "version": "0.0.2", | ||
| "version": "0.0.3", | ||
| "description": "Prevent duplicates in your data by rejecting duplicate events at ingestion.", | ||
@@ -5,0 +5,0 @@ "main": "index.ts", |
+1
-1
@@ -5,3 +5,3 @@ { | ||
| "description": "Prevent duplicates in your data when ingesting.", | ||
| "main": "index.js", | ||
| "main": "index.ts", | ||
| "posthogVersion": ">=1.25.0", | ||
@@ -8,0 +8,0 @@ "config": [ |
-51
| import { createHash, randomUUID } from 'crypto'; | ||
| const NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8'; | ||
| const stringifyEvent = (event) => { | ||
| return `(${randomUUID().toString()}; project #${event.team_id}). Event "${event.event}" @ ${event.timestamp} for user ${event.distinct_id}.`; | ||
| }; | ||
| const byteToHex = []; | ||
| for (let i = 0; i < 256; ++i) { | ||
| byteToHex.push((i + 0x100).toString(16).slice(1)); | ||
| } | ||
| function stringifyUUID(arr) { | ||
| return (byteToHex[arr[0]] + | ||
| byteToHex[arr[1]] + | ||
| byteToHex[arr[2]] + | ||
| byteToHex[arr[3]] + | ||
| '-' + | ||
| byteToHex[arr[4]] + | ||
| byteToHex[arr[5]] + | ||
| '-' + | ||
| byteToHex[arr[6]] + | ||
| byteToHex[arr[7]] + | ||
| '-' + | ||
| byteToHex[arr[8]] + | ||
| byteToHex[arr[9]] + | ||
| '-' + | ||
| byteToHex[arr[10]] + | ||
| byteToHex[arr[11]] + | ||
| byteToHex[arr[12]] + | ||
| byteToHex[arr[13]] + | ||
| byteToHex[arr[14]] + | ||
| byteToHex[arr[15]]).toLowerCase(); | ||
| } | ||
| const plugin = { | ||
| processEvent: async (event, { config }) => { | ||
| stringifyEvent(event); | ||
| if (!event.timestamp) { | ||
| console.info('Received event without a timestamp, the event will not be processed because deduping will not work.'); | ||
| return event; | ||
| } | ||
| const stringifiedProps = config.dedupMode === 'All Properties' ? `_${JSON.stringify(event.properties)}` : ''; | ||
| const hash = createHash('sha1'); | ||
| const eventKeyBuffer = hash | ||
| .update(`${NAMESPACE_OID}_${event.team_id}_${event.distinct_id}_${event.event}_${event.timestamp}${stringifiedProps}`) | ||
| .digest(); | ||
| eventKeyBuffer[6] = (eventKeyBuffer[6] & 0x0f) | 0x50; | ||
| eventKeyBuffer[8] = (eventKeyBuffer[8] & 0x3f) | 0x80; | ||
| event.uuid = stringifyUUID(eventKeyBuffer); | ||
| return event; | ||
| }, | ||
| }; | ||
| module.exports = plugin; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
53516
20.04%15
150%251
280.3%1
Infinity%