Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New fsApi / version 2 support #235

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

New fsApi / version 2 support #235

wants to merge 10 commits into from

Conversation

irv-fs
Copy link
Contributor

@irv-fs irv-fs commented Dec 10, 2024

No description provided.

… checks for that vs destination

# [solrdeploy] uncomment this line if this requires solr deploy.
… checks for that vs destination

# [solrdeploy] uncomment this line if this requires solr deploy.
# [solrdeploy] uncomment this line if this requires solr deploy.
# [solrdeploy] uncomment this line if this requires solr deploy.
@irv-fs irv-fs assigned irv-fs, ndonze-fs and van-fs and unassigned irv-fs Dec 10, 2024
# [solrdeploy] uncomment this line if this requires solr deploy.
# [solrdeploy] uncomment this line if this requires solr deploy.
src/observer.ts Outdated
@@ -9,6 +9,10 @@ import { FunctionOperator } from './operators';
import DataLayerTarget from './target';
import MonitorFactory from './monitor-factory';
import { errorType, Telemetry, telemetryType } from './utils/telemetry';
import TrackEventOperator from './operators/fsApi/trackEvent';
Copy link
Member

Choose a reason for hiding this comment

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

nit: these can be exported from the index.ts in /operators and imported as a single statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

src/observer.ts Outdated
readOnLoad?: boolean;
url?: string;
id?: string;
description?: string;
monitor?: boolean;
waitUntil?: number | Function;
maxRetry?: number;
version?: number;
fsApi?: string;
Copy link
Member

Choose a reason for hiding this comment

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

fsApi is one of FS_API_CONSTANTS, no? Should that be the type and not string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

src/observer.ts Outdated
if (fsApi) {
switch (fsApi) {
case FS_API_CONSTANTS.SET_IDENTITY:
handler.push(new SetIdentityOperator({ name: 'setIdentity' }));
Copy link
Member

@van-fs van-fs Dec 14, 2024

Choose a reason for hiding this comment

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

We could just reuse the FS_API_CONSTANTS member as the value of name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

src/observer.ts Outdated
*/
private addOperators(handler: DataHandler, options: OperatorOptions[], destination: string | Function) {
private addOperators(handler: DataHandler, options: OperatorOptions[],
destination: string | Function | undefined = undefined, fsApi: string | undefined = undefined,
Copy link
Member

@van-fs van-fs Dec 14, 2024

Choose a reason for hiding this comment

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

destination and fsApi can be declared optional with destination? and fsApi?. Is there a need to add a union type with undefined and assign it undefined? That would be automatic with the optional.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I started with this, but optional parameters have to go at the end, and version is optional but with a hardcoded default value. I'd have to move version in front of them, and then anyone using the api would have to put a version in if they wanted to specify destination or fsApi. The other option would be to give these default values of undefined as well. Thoughts?

return;
}

if (destination && fsApi) {
Copy link
Member

Choose a reason for hiding this comment

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

While conceptually incorrect (competing destinations), I wonder if we should prefer fsApi if it is also used with destination. In this scenario, we still still send data versus nothing. We could emit an error to indicate the problem, and the user would be able to see the event in the replay along with the indication there are competing destinations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tend to think if someone specified both it was probably done in error, and I'd rather bomb out than have them hunt down why it isn't working. This happens with DLO unfortunately and it can be tricky to figure out why things are broken. But not a hill for me to die on either, so could change it.

// end with destination
const func = previewMode ? previewDestination : destination;
handler.push(new FunctionOperator({ name: 'function', func }));
if (fsApi) {
Copy link
Member

Choose a reason for hiding this comment

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

Given these are operators, my skew is that we re-use the OperatorFactory and this.getOperator to instantiate operators. I may have to get into the code to see if that creates unwanted complexity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted it explicit in the code, but could move those into registered operators with an OperatorFactory if desired. I tend to favor explicit code in special situations like this, but not a hill for me to die on.

export default class SetPagePropertiesOperator extends FSApiOperator {
// eslint-disable-next-line class-methods-use-this
prepareData(inputData:any[]): any[] | null {
if (inputData === null || inputData.length < 1) {
Copy link
Member

Choose a reason for hiding this comment

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

Should we throw an Error? The operator expects inputData, right? Is there a scenario where not receiving data but expecting it to be sent to setProperties is OK? Returning null is typically used to halt the operator chain under expected conditions - like when a query doesn't match and the chain is expected to stop.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Contributor

@ScottLNorvell ScottLNorvell left a comment

Choose a reason for hiding this comment

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

Hi @irv-fs ! Thanks for your patience on this. Everything looks good here. I'm going to go schedule some time to go over this with @ndonze-fs to make sure I understand what's going on and then we'll stamp

// useful if every rule needs the same operator run before the destination
if (beforeDestination) {
if (beforeDestination && (version === 1)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Will this prevent the application of any beforeDestination operators? If so, is this safe/intended? I expect we're trying to avoid having the suffix operator added. However, we also convert by default and insert the dlo capture source (not sure how capture sources work in v2 APIs).

[{ name: 'convert', enumerate: true, preserveArray: true, index: -1 },{ name: 'suffix' },{ name: 'insert', value: 'dlo', position: -1 }]

I wonder if instead we should be thinking about noop'ing the suffix operator given v2 client APIs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, the way this is coded, if you use V2 of DLC you are in charge of putting all of your operators that are needed, which would include the convert operator. I do automatically insert dlo as an additional parameter in the fsApi base operator, so that piece is taken care of. You can use the new fsApi stuff with version 1 however if desired.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was very nervous to just make the suffix operator a no-op. I know we have done a lot of work to get around suffixes, allowing people to use specific names. I instead decided to introduce a rule version, which I think gives us more flexibility. Anybody who wants this new functionality needs to explicitly set the rule version, which they knowingly opt into. We can also use this for future backwards incompatible changes at the rule level. I think it gives us another lever that was missing in the past.

Copy link
Contributor

Choose a reason for hiding this comment

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

A side effect of nixing beforeDestinationOperators is that moving to FS API V2 is coupled with a capture behavior change. Previously, we added convert for you. Now, given the same set of rules upgraded to V2, we'll no longer convert. if you had a string amount e.g. "10.75", we would've previously been converting that to a real. Now, we'll no longer do that. So, we both stop adding suffixes (good relative to V2) and we change the capture behavior. If a customer would be negatively impacted by the change in capture behavior (e.g. now having mixed real and str values), they can't be easily be migrated to FS API V2.

This coupling doesn't feel great for me as it limits the on-ramp to V2. I wonder if it's worth soliciting a product perspective on this decision.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When a customer moves to V2, it won't be as easy as flipping a switch or setting a simple attribute as each rule will have to be visited as some inserts, etc will need to be removed. The structure of V2 is just different, so it won't be a simple path regardless. Forcing them to also consider convert seems small compared to what is already needed, and gets us away from the hardcoded beforeDestination stuff while keeping backwards compatibility.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you provide examples of other concerns we'll have moving customers from V1 to V2? You mentioned the structure of V2 being different which suggests that -- even if we were to position the V2 API as a destination instead of an operator -- we wouldn't be able to take an existing operator chain and have it call V2 APIs without capture behavior changes.

I'm pulling on this thread because in the past we've seen capture behavior changes (e.g. when we've changed beforeDestinationOperators) as breaking changes which motivated us to:

  • Bump the DLO major version
  • Feature switch such that we could migrate customers between major versions over time

In this case, it's a little different because we're introducing a rule version within the overall DLO library version.

Between data sinks becoming operators and opting into breaking changes via rule specs, we're shifting responsibilities around vs. where they've lived in the past. This may be okay, though I think we should have a good awareness of these shifts as they may complicate what changes we can make and where over time e.g. not being able to count on beforeDestinationOperators to make changes with client integration scripts since we can no longer count on those to be effective for all customers.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking back on an earlier comment, is this true?

You can use the new fsApi stuff with version 1 however if desired.

With FS API calls currently being positioned as operators instead of destinations, wouldn't the operator to call FS API V2 execute before the beforeDestinationOperators which are appended to the end of the operators array?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Opening up a rule version would allow those writing rules to gradually migrate as they see fit, even rule by rule, versus an all or nothing change, and doesn't require any major hassles on our end (as far as migrating folks). Given that most of the rule writers are inside Fullstory, this seems to be a benefit?

private addOperators(handler: DataHandler, options: OperatorOptions[], destination: string | Function) {
private addOperators(handler: DataHandler, options: OperatorOptions[],
destination: string | Function | undefined = undefined, fsApi: string | undefined = undefined,
version: number = 1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

version feels generic here without a fuller name/jsdoc. Wonder if we should make this clear that we're referring to FS client API version -- and maybe further constrain to known versions with a union instead of using a number.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added jsDoc. That was an oversight.

irv-fs added 2 commits January 2, 2025 13:53
# [solrdeploy] uncomment this line if this requires solr deploy.
# [solrdeploy] uncomment this line if this requires solr deploy.
Telemetry.error(errorType.invalidRuleError);
return;
}

// sanity check destination and fsApi parameters
if (!destination && !fsApi) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I've been thinking a bit more on this. If we think about DLO as ETL (extract from source, transform through operators, load through destination), as implemented this positions the FS API V2 destination as an operator rather than a destination. How do we feel about this overall positioning? Operators can now be transformations, or they could actually be sending data -- whereas previously there was a separation between operators (transformations) and destinations (data sinks). As a side effect, this kind of forces us away from beforeDestinationOperators because with V2 there is no destination.

As a potential alternative to consider, we could position destination as a string (as is) or as an FS API V2 destination which would be a more complex destination object. As a crude example, destination: { target: 'fs', method: 'setIdentity' }.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In my design doc I had originally positioned the V2 as just a special string inside a destination, but it seemed more folks wanted the separate attribute (which I also prefer as it is clearer the intention).

Copy link
Contributor

Choose a reason for hiding this comment

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

In this case, we wouldn't have a special string; rather, we would have a more complex destination object that wouldn't look terribly different than operator definitions. That said, this is not a hill I will die on if other folks think it's sensible to start merging the ideas of operators/transformations and data sinks. 😆

// useful if every rule needs the same operator run before the destination
if (beforeDestination) {
if (beforeDestination && (version === 1)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

A side effect of nixing beforeDestinationOperators is that moving to FS API V2 is coupled with a capture behavior change. Previously, we added convert for you. Now, given the same set of rules upgraded to V2, we'll no longer convert. if you had a string amount e.g. "10.75", we would've previously been converting that to a real. Now, we'll no longer do that. So, we both stop adding suffixes (good relative to V2) and we change the capture behavior. If a customer would be negatively impacted by the change in capture behavior (e.g. now having mixed real and str values), they can't be easily be migrated to FS API V2.

This coupling doesn't feel great for me as it limits the on-ramp to V2. I wonder if it's worth soliciting a product perspective on this decision.

Copy link
Contributor

@ScottLNorvell ScottLNorvell left a comment

Choose a reason for hiding this comment

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

This all looks good from my end. I think once @ndonze-fs signs off we can get this deployed.

Sorry for the delay on this.

# [solrdeploy] uncomment this line if this requires solr deploy./gd
Copy link
Contributor

@ndonze-fs ndonze-fs left a comment

Choose a reason for hiding this comment

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

LGTM

# [solrdeploy] uncomment this line if this requires solr deploy./gd
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.

4 participants