Skip to content

Commit 92f7456

Browse files
update filename
1 parent f9e510a commit 92f7456

File tree

3 files changed

+264
-264
lines changed

3 files changed

+264
-264
lines changed
Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
// Copyright (c) Microsoft Corporation.
2-
// Licensed under the MIT license.
3-
4-
export interface IFeatureFilter {
5-
name: string; // e.g. Microsoft.TimeWindow
6-
evaluate(context: IFeatureFilterEvaluationContext, appContext?: unknown): boolean | Promise<boolean>;
7-
}
8-
9-
export interface IFeatureFilterEvaluationContext {
10-
featureName: string;
11-
parameters?: unknown;
12-
}
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
export interface IFeatureFilter {
5+
name: string; // e.g. Microsoft.TimeWindow
6+
evaluate(context: IFeatureFilterEvaluationContext, appContext?: unknown): boolean | Promise<boolean>;
7+
}
8+
9+
export interface IFeatureFilterEvaluationContext {
10+
featureName: string;
11+
parameters?: unknown;
12+
}
Lines changed: 172 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,172 +1,172 @@
1-
// Copyright (c) Microsoft Corporation.
2-
// Licensed under the MIT license.
3-
4-
import { IFeatureFilter } from "./featureFilter.js";
5-
6-
type TargetingFilterParameters = {
7-
Audience: {
8-
DefaultRolloutPercentage: number;
9-
Users?: string[];
10-
Groups?: {
11-
Name: string;
12-
RolloutPercentage: number;
13-
}[];
14-
Exclusion?: {
15-
Users?: string[];
16-
Groups?: string[];
17-
};
18-
}
19-
}
20-
21-
type TargetingFilterEvaluationContext = {
22-
featureName: string;
23-
parameters: TargetingFilterParameters;
24-
}
25-
26-
type TargetingFilterAppContext = {
27-
userId?: string;
28-
groups?: string[];
29-
}
30-
31-
export class TargetingFilter implements IFeatureFilter {
32-
name: string = "Microsoft.Targeting";
33-
34-
async evaluate(context: TargetingFilterEvaluationContext, appContext?: TargetingFilterAppContext): Promise<boolean> {
35-
const { featureName, parameters } = context;
36-
TargetingFilter.#validateParameters(parameters);
37-
38-
if (appContext === undefined) {
39-
throw new Error("The app context is required for targeting filter.");
40-
}
41-
42-
if (parameters.Audience.Exclusion !== undefined) {
43-
// check if the user is in the exclusion list
44-
if (appContext?.userId !== undefined &&
45-
parameters.Audience.Exclusion.Users !== undefined &&
46-
parameters.Audience.Exclusion.Users.includes(appContext.userId)) {
47-
return false;
48-
}
49-
// check if the user is in a group within exclusion list
50-
if (appContext?.groups !== undefined &&
51-
parameters.Audience.Exclusion.Groups !== undefined) {
52-
for (const excludedGroup of parameters.Audience.Exclusion.Groups) {
53-
if (appContext.groups.includes(excludedGroup)) {
54-
return false;
55-
}
56-
}
57-
}
58-
}
59-
60-
// check if the user is being targeted directly
61-
if (appContext?.userId !== undefined &&
62-
parameters.Audience.Users !== undefined &&
63-
parameters.Audience.Users.includes(appContext.userId)) {
64-
return true;
65-
}
66-
67-
// check if the user is in a group that is being targeted
68-
if (appContext?.groups !== undefined &&
69-
parameters.Audience.Groups !== undefined) {
70-
for (const group of parameters.Audience.Groups) {
71-
if (appContext.groups.includes(group.Name)) {
72-
const audienceContextId = constructAudienceContextId(featureName, appContext.userId, group.Name);
73-
const rolloutPercentage = group.RolloutPercentage;
74-
if (await TargetingFilter.#isTargeted(audienceContextId, rolloutPercentage)) {
75-
return true;
76-
}
77-
}
78-
}
79-
}
80-
81-
// check if the user is being targeted by a default rollout percentage
82-
const defaultContextId = constructAudienceContextId(featureName, appContext?.userId);
83-
return TargetingFilter.#isTargeted(defaultContextId, parameters.Audience.DefaultRolloutPercentage);
84-
}
85-
86-
static async #isTargeted(audienceContextId: string, rolloutPercentage: number): Promise<boolean> {
87-
if (rolloutPercentage === 100) {
88-
return true;
89-
}
90-
// Cryptographic hashing algorithms ensure adequate entropy across hash values.
91-
const contextMarker = await stringToUint32(audienceContextId);
92-
const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100;
93-
return contextPercentage < rolloutPercentage;
94-
}
95-
96-
static #validateParameters(parameters: TargetingFilterParameters): void {
97-
if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) {
98-
throw new Error("Audience.DefaultRolloutPercentage must be a number between 0 and 100.");
99-
}
100-
// validate RolloutPercentage for each group
101-
if (parameters.Audience.Groups !== undefined) {
102-
for (const group of parameters.Audience.Groups) {
103-
if (group.RolloutPercentage < 0 || group.RolloutPercentage > 100) {
104-
throw new Error(`RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`);
105-
}
106-
}
107-
}
108-
}
109-
}
110-
111-
/**
112-
* Constructs the context id for the audience.
113-
* The context id is used to determine if the user is part of the audience for a feature.
114-
* If groupName is provided, the context id is constructed as follows:
115-
* userId + "\n" + featureName + "\n" + groupName
116-
* Otherwise, the context id is constructed as follows:
117-
* userId + "\n" + featureName
118-
*
119-
* @param featureName name of the feature
120-
* @param userId userId from app context
121-
* @param groupName group name from app context
122-
* @returns a string that represents the context id for the audience
123-
*/
124-
function constructAudienceContextId(featureName: string, userId: string | undefined, groupName?: string) {
125-
let contextId = `${userId ?? ""}\n${featureName}`;
126-
if (groupName !== undefined) {
127-
contextId += `\n${groupName}`;
128-
}
129-
return contextId;
130-
}
131-
132-
async function stringToUint32(str: string): Promise<number> {
133-
let crypto;
134-
135-
// Check for browser environment
136-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
137-
crypto = window.crypto;
138-
}
139-
// Check for Node.js environment
140-
else if (typeof global !== "undefined" && global.crypto) {
141-
crypto = global.crypto;
142-
}
143-
// Fallback to native Node.js crypto module
144-
else {
145-
try {
146-
if (typeof module !== "undefined" && module.exports) {
147-
crypto = require("crypto");
148-
}
149-
else {
150-
crypto = await import("crypto");
151-
}
152-
} catch (error) {
153-
console.error("Failed to load the crypto module:", error.message);
154-
throw error;
155-
}
156-
}
157-
158-
// In the browser, use crypto.subtle.digest
159-
if (crypto.subtle) {
160-
const data = new TextEncoder().encode(str);
161-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
162-
const dataView = new DataView(hashBuffer);
163-
const uint32 = dataView.getUint32(0, true);
164-
return uint32;
165-
}
166-
// In Node.js, use the crypto module's hash function
167-
else {
168-
const hash = crypto.createHash("sha256").update(str).digest();
169-
const uint32 = hash.readUInt32LE(0);
170-
return uint32;
171-
}
172-
}
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { IFeatureFilter } from "./featureFilter.js";
5+
6+
type TargetingFilterParameters = {
7+
Audience: {
8+
DefaultRolloutPercentage: number;
9+
Users?: string[];
10+
Groups?: {
11+
Name: string;
12+
RolloutPercentage: number;
13+
}[];
14+
Exclusion?: {
15+
Users?: string[];
16+
Groups?: string[];
17+
};
18+
}
19+
}
20+
21+
type TargetingFilterEvaluationContext = {
22+
featureName: string;
23+
parameters: TargetingFilterParameters;
24+
}
25+
26+
type TargetingFilterAppContext = {
27+
userId?: string;
28+
groups?: string[];
29+
}
30+
31+
export class TargetingFilter implements IFeatureFilter {
32+
name: string = "Microsoft.Targeting";
33+
34+
async evaluate(context: TargetingFilterEvaluationContext, appContext?: TargetingFilterAppContext): Promise<boolean> {
35+
const { featureName, parameters } = context;
36+
TargetingFilter.#validateParameters(parameters);
37+
38+
if (appContext === undefined) {
39+
throw new Error("The app context is required for targeting filter.");
40+
}
41+
42+
if (parameters.Audience.Exclusion !== undefined) {
43+
// check if the user is in the exclusion list
44+
if (appContext?.userId !== undefined &&
45+
parameters.Audience.Exclusion.Users !== undefined &&
46+
parameters.Audience.Exclusion.Users.includes(appContext.userId)) {
47+
return false;
48+
}
49+
// check if the user is in a group within exclusion list
50+
if (appContext?.groups !== undefined &&
51+
parameters.Audience.Exclusion.Groups !== undefined) {
52+
for (const excludedGroup of parameters.Audience.Exclusion.Groups) {
53+
if (appContext.groups.includes(excludedGroup)) {
54+
return false;
55+
}
56+
}
57+
}
58+
}
59+
60+
// check if the user is being targeted directly
61+
if (appContext?.userId !== undefined &&
62+
parameters.Audience.Users !== undefined &&
63+
parameters.Audience.Users.includes(appContext.userId)) {
64+
return true;
65+
}
66+
67+
// check if the user is in a group that is being targeted
68+
if (appContext?.groups !== undefined &&
69+
parameters.Audience.Groups !== undefined) {
70+
for (const group of parameters.Audience.Groups) {
71+
if (appContext.groups.includes(group.Name)) {
72+
const audienceContextId = constructAudienceContextId(featureName, appContext.userId, group.Name);
73+
const rolloutPercentage = group.RolloutPercentage;
74+
if (await TargetingFilter.#isTargeted(audienceContextId, rolloutPercentage)) {
75+
return true;
76+
}
77+
}
78+
}
79+
}
80+
81+
// check if the user is being targeted by a default rollout percentage
82+
const defaultContextId = constructAudienceContextId(featureName, appContext?.userId);
83+
return TargetingFilter.#isTargeted(defaultContextId, parameters.Audience.DefaultRolloutPercentage);
84+
}
85+
86+
static async #isTargeted(audienceContextId: string, rolloutPercentage: number): Promise<boolean> {
87+
if (rolloutPercentage === 100) {
88+
return true;
89+
}
90+
// Cryptographic hashing algorithms ensure adequate entropy across hash values.
91+
const contextMarker = await stringToUint32(audienceContextId);
92+
const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100;
93+
return contextPercentage < rolloutPercentage;
94+
}
95+
96+
static #validateParameters(parameters: TargetingFilterParameters): void {
97+
if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) {
98+
throw new Error("Audience.DefaultRolloutPercentage must be a number between 0 and 100.");
99+
}
100+
// validate RolloutPercentage for each group
101+
if (parameters.Audience.Groups !== undefined) {
102+
for (const group of parameters.Audience.Groups) {
103+
if (group.RolloutPercentage < 0 || group.RolloutPercentage > 100) {
104+
throw new Error(`RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`);
105+
}
106+
}
107+
}
108+
}
109+
}
110+
111+
/**
112+
* Constructs the context id for the audience.
113+
* The context id is used to determine if the user is part of the audience for a feature.
114+
* If groupName is provided, the context id is constructed as follows:
115+
* userId + "\n" + featureName + "\n" + groupName
116+
* Otherwise, the context id is constructed as follows:
117+
* userId + "\n" + featureName
118+
*
119+
* @param featureName name of the feature
120+
* @param userId userId from app context
121+
* @param groupName group name from app context
122+
* @returns a string that represents the context id for the audience
123+
*/
124+
function constructAudienceContextId(featureName: string, userId: string | undefined, groupName?: string) {
125+
let contextId = `${userId ?? ""}\n${featureName}`;
126+
if (groupName !== undefined) {
127+
contextId += `\n${groupName}`;
128+
}
129+
return contextId;
130+
}
131+
132+
async function stringToUint32(str: string): Promise<number> {
133+
let crypto;
134+
135+
// Check for browser environment
136+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
137+
crypto = window.crypto;
138+
}
139+
// Check for Node.js environment
140+
else if (typeof global !== "undefined" && global.crypto) {
141+
crypto = global.crypto;
142+
}
143+
// Fallback to native Node.js crypto module
144+
else {
145+
try {
146+
if (typeof module !== "undefined" && module.exports) {
147+
crypto = require("crypto");
148+
}
149+
else {
150+
crypto = await import("crypto");
151+
}
152+
} catch (error) {
153+
console.error("Failed to load the crypto module:", error.message);
154+
throw error;
155+
}
156+
}
157+
158+
// In the browser, use crypto.subtle.digest
159+
if (crypto.subtle) {
160+
const data = new TextEncoder().encode(str);
161+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
162+
const dataView = new DataView(hashBuffer);
163+
const uint32 = dataView.getUint32(0, true);
164+
return uint32;
165+
}
166+
// In Node.js, use the crypto module's hash function
167+
else {
168+
const hash = crypto.createHash("sha256").update(str).digest();
169+
const uint32 = hash.readUInt32LE(0);
170+
return uint32;
171+
}
172+
}

0 commit comments

Comments
 (0)