Skip to content

Conversation

felipecsl
Copy link
Contributor

Towards: FF-3567

Motivation and Context

Add ability to track and deliver arbitrary events to an ingestion URL

Description

  • Create EventDispatcher to encapsulate event queueing and delivery attempts
  • Added retry mechanism with BatchRetryManager with configurable settings
  • Added NetworkStatusListener interface (implementation will be platform specific) to handle online/offline mode
  • Upload events in batches of configurable size

How has this been tested?

Wrote tests

}

const DEFAULT_EVENT_DISPATCHER_CONFIG = {
// TODO: Replace with actual ingestion URL
Copy link
Contributor Author

@felipecsl felipecsl Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be handled in a follow up PR by parsing it from the SDK key


constructor({
eventQueue = new ArrayBackedNamedEventQueue('events'),
eventDispatcher = new NoOpEventDispatcher(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default implementation is a no-op if none provided

// noinspection JSUnusedGlobalSymbols
track(event: unknown, params: Record<string, unknown>) {
this.eventQueue.push({ event, params });
this.eventDispatcher.dispatch({ id: randomUUID(), data: event, params });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a random uuid for avoiding duplicates server side

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking the payload would contain 4 fields:

  • UUID
  • timestamp
  • event type (hard-coded/not modifiable without SDK code changes)
  • payload

What is intended use of data vs params here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is intended use of data vs params here?

That's pretty arbitrary and will change. I wouldn't worry too much about it just yet, since we're not using the actual types here, it's a placeholder.

if (attempt < maxRetries) {
return this.retry(batch, attempt + 1);
} else {
// TODO: Persist batch to avoid data loss
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will also do this in a future PR

"pino": "^8.19.0",
"semver": "^7.5.4"
"semver": "^7.5.4",
"uuid": "^8.3.2"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a fairly old version of uuid (4 years old) but the last one that's compatible with our module settings

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we don't want to add an additional dependency, we can delegate the event ID generation to consuming libraries (js-client, node, etc)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you hoping to use uuid v7 because of this https://buildkite.com/resources/blog/goodbye-integers-hello-uuids/ or another feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately we can't use uuid v7 because it's not available in the version of the library we're using. looks like it was added on uuid v10. however yes, that could be a pretty decent primary key for these events. I don't want to make any assumptions here, the only goal was to have a unique identifier and we can't use sequential IDs reliably because we can't guarantee the sequence ordering (we'd have to persist additional state for the last ID used, which is inconvenient).

Copy link
Member

@leoromanovsky leoromanovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great tests - this library is coming along great. can't wait to use it for the telemetry project!

"sourceMap": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"target": "es2020",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

Comment on lines +16 to +19
expect(batch).toEqual([
{ id: 'foo-1', data: 'event1', params: {} },
{ id: 'foo-2', data: 'event2', params: {} },
]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

@felipecsl
Copy link
Contributor Author

this was co-authored with chatgpt 😂

ingestionUrl: 'https://example.com/events',
batchSize: 10,
flushInterval: 10_000,
retryInterval: 5_000,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include the unit in this (and flush interval), so retryIntervalMs and flushIntervalMs?

// noinspection JSUnusedGlobalSymbols
track(event: unknown, params: Record<string, unknown>) {
this.eventQueue.push({ event, params });
this.eventDispatcher.dispatch({ id: randomUUID(), data: event, params });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking the payload would contain 4 fields:

  • UUID
  • timestamp
  • event type (hard-coded/not modifiable without SDK code changes)
  • payload

What is intended use of data vs params here?

/**
* @internal
* A named event queue backed by an **unbounded** array.
* This class probably should NOT be used directly, but only as a backing store for
Copy link
Contributor

@petzel petzel Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this shouldn't ever be used directly, I think this should be built in to BoundedEventQueue and not a separate class. We can create it as a separate class it/when we need BoundedEventQueue to support a different backing store.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BoundedEventQueue uses the NamedEventQueue interface, which is also used by BatchEventProcessor.
ArrayBackedNamedEventQueue is also useful for tests, I think it's a useful abstraction that I'd like to keep around for internal use.
I'm gonna remove it from the exports in index.ts, so no consumers should be able to use it. Let me know if that doesn't address your concerns about it

maxRetries: config.maxRetries || 3,
});
this.deliveryIntervalMs = config.deliveryIntervalMs;
this.networkStatusListener.onNetworkStatusChange((isOffline) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this listener getting invoked anywhere - how will this work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concrete implementations for each platform will be provided by consuming libraries (node-sdk and js-client-sdk)

@felipecsl felipecsl merged commit 7904d04 into main Nov 25, 2024
8 checks passed
@felipecsl felipecsl deleted the felipecsl--event-dispatch branch November 25, 2024 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants