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

chore!: deprecate reason for ArcjetAllowDecision #2715

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

Conversation

e-moran
Copy link
Contributor

@e-moran e-moran commented Jan 2, 2025

This PR makes reason optional for ArcjetAllowDecision and deprecates it.

We are making this change because the rule that is accessible through the reason field is not obvious to our users and has the potential to cause confusion.

Instead we recommend iterating through the ruleResults list to find rule you are looking for.

@e-moran e-moran requested a review from a team as a code owner January 2, 2025 16:49
Copy link

trunk-io bot commented Jan 2, 2025

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

@davidmytton
Copy link
Contributor

Can you add the migration example code to this PR description please? It should be documented in the changelog as well, but it'll be helpful to have it in this PR in case someone comes across it.

@e-moran e-moran force-pushed the eoin/deprecate-reason-for-allow branch from 252d92f to 8b78916 Compare January 2, 2025 17:49
@e-moran e-moran changed the title deprecate!: deprecate reason for ArcjetAllowDecision chore!: deprecate reason for ArcjetAllowDecision Jan 2, 2025
@@ -208,7 +208,7 @@ export function setRateLimitHeaders(
.sort(sortByLowestMax)
.map(toPolicyString)
.join(", ");
} else {
} else if (typeof decision.reason !== "undefined") {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why doesn't isRateLimitReason just taking an optional reason?

examples/nextjs-bot-categories/app/api/arcjet/route.ts Outdated Show resolved Hide resolved
examples/nextjs-clerk-rate-limit/app/api/arcjet/route.ts Outdated Show resolved Hide resolved
examples/nextjs-openai/app/api/chat/route.ts Outdated Show resolved Hide resolved
protocol/test/convert.test.ts Outdated Show resolved Hide resolved
@@ -591,7 +591,7 @@ export abstract class ArcjetDecision {
ip: ArcjetIpDetails;

abstract conclusion: ArcjetConclusion;
abstract reason: ArcjetReason;
abstract reason: ArcjetReason | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

This stinks. We might want to remove these abstract fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They could be useful if someone wants to type-check on a generic ArcjetDecision to see if it has a reason or not.

Copy link
Contributor

Choose a reason for hiding this comment

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

I need to double check, but I don't think this needs to be abstract to do that.

Comment on lines 630 to 632
export type ArcjetDecisionWithReason = ArcjetDecision & {
reason: NonNullable<ArcjetDecision["reason"]>;
};
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 like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you not like its existence, or just how its defined?
I could change it to just ArcjetDecision & { reason: ArcjetReason}, or else we'd have to use that definition in every function that requires a reason.

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 think we need to provide it because they could write their own intersection. Right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah they could! I've removed it

Comment on lines 123 to 141
const policies = new Map<number, number>();
for (const reason of rateLimitReasons) {
if (policies.has(reason.max)) {
console.error(
"Invalid rate limit policy—two policies should not share the same limit",
);
}

if (
typeof reason.max !== "number" ||
typeof reason.window !== "number" ||
typeof reason.remaining !== "number" ||
typeof reason.reset !== "number"
) {
console.error(format("Invalid rate limit encountered: %o", reason));
}

policies.set(reason.max, reason.window);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Policies isn't doing anything? Did you want to break out of the logic or something?

Copy link
Contributor Author

@e-moran e-moran Jan 10, 2025

Choose a reason for hiding this comment

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

Its used to print an error if two rate limit policies share the same limit, like we do in decorate. The difference is that decorate returns an error in this case. We could respond with a 500 here to have the same behavior. What do you think?

Another option would be to detect invalid configs earlier, such as when initializing Arcjet. But that would be a much larger change.

@@ -10,6 +10,7 @@
},
"dependencies": {
"@arcjet/next": "file:../../arcjet-next",
"@arcjet/sprintf": "file:../../sprintf",
Copy link
Contributor

Choose a reason for hiding this comment

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

Might not be needed if you don't keep the policies stuff

console.error(format("Invalid rate limit encountered: %o", reason));
}

policies.set(reason.max, reason.window);
Copy link
Contributor

Choose a reason for hiding this comment

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

see above

@@ -11,6 +11,7 @@
"dependencies": {
"@ai-sdk/openai": "^1.0.8",
"@arcjet/next": "file:../../arcjet-next",
"@arcjet/sprintf": "file:../../sprintf",
Copy link
Contributor

Choose a reason for hiding this comment

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

see above

Comment on lines 630 to 632
export type ArcjetDecisionWithReason = ArcjetDecision & {
reason: NonNullable<ArcjetDecision["reason"]>;
};
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 think we need to provide it because they could write their own intersection. Right?

{ status: 403, headers },
);
}
const allowedBots = decision.results.reduce<string[]>(reduceAllowedBots, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed you are still using map/filter in the other examples (for rate limit). We should keep it uniform. The reason to use a reduce is to avoid creating extra arrays, but the implementation of the reduction function is creating a new array on each iteration which is worse than a single filter then a map.

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've changed this to a reduce. This removes checking that a limit isn't duplicated which is discussed in another comment though.

@@ -127,7 +128,7 @@ export function createClient(options: ClientOptions): Client {
report(
context: ArcjetContext,
details: ArcjetRequestDetails,
decision: ArcjetDecision,
decision: ArcjetDecisionWithReason,
Copy link
Contributor

Choose a reason for hiding this comment

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

It doesn't need to have a reason, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

What happened with also deprecating reason on ArcjetErrorDecision?

export class ArcjetAllowDecision extends ArcjetDecision {
conclusion = "ALLOW" as const;
reason: ArcjetReason;
/** @deprecated */
Copy link
Contributor

Choose a reason for hiding this comment

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

needs a message

@@ -591,7 +591,7 @@ export abstract class ArcjetDecision {
ip: ArcjetIpDetails;

abstract conclusion: ArcjetConclusion;
abstract reason: ArcjetReason;
abstract reason: ArcjetReason | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

I need to double check, but I don't think this needs to be abstract to do that.

trunk-io bot pushed a commit that referenced this pull request Jan 14, 2025
…2855)

While reviewing #2715 locally, I noticed that `/** @deprecated */` wasn't being marked in my editor. This is because `/** */` is just a block comment and not a doc comment.

Doc comments need to be in the form as I've changed this PR. Additionally, deprecating the top-level export didn't seem to mark anything in the editor, so each field needed to be marked as deprecated. Furthermore, overriding the type as `ArcjetEnum<>` seemed to lose the doc comments.

I also removed the exports locally and made everything compile once they are removed. They still exist for now but I want the removal to be seamless.
@blaine-arcjet blaine-arcjet marked this pull request as draft January 15, 2025 17:14
@blaine-arcjet
Copy link
Contributor

Converting to draft while we discuss this further.

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