Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions __tests__/common/number.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { applyDeterministicVariation } from '../../src/common/number';

describe('applyDeterministicVariation', () => {
const baseValue = 1000;
const maxVariationPercent = 7.6;

it('should return deterministic results for the same seed', () => {
const seed = 'test-opportunity-id-123';

const result1 = applyDeterministicVariation({
value: baseValue,
seed,
maxVariationPercent,
});
const result2 = applyDeterministicVariation({
value: baseValue,
seed,
maxVariationPercent,
});

expect(result1).toBe(result2);
});

it('should return different results for different seeds', () => {
const result1 = applyDeterministicVariation({
value: baseValue,
seed: 'completely-different-seed-alpha',
maxVariationPercent,
});
const result2 = applyDeterministicVariation({
value: baseValue,
seed: 'another-unique-identifier-beta',
maxVariationPercent,
});

expect(result1).not.toBe(result2);
});

it('should return results within the expected range', () => {
const seeds = [
'abc',
'xyz',
'123',
'test',
'opportunity-1',
'opportunity-2',
'some-uuid-here',
'another-id',
'short',
'a-very-long-seed-string-to-test-with',
];

const minExpected = Math.round(baseValue * (1 - maxVariationPercent / 100));
const maxExpected = Math.round(baseValue * (1 + maxVariationPercent / 100));

seeds.forEach((seed) => {
const result = applyDeterministicVariation({
value: baseValue,
seed,
maxVariationPercent,
});

expect(result).toBeGreaterThanOrEqual(minExpected);
expect(result).toBeLessThanOrEqual(maxExpected);
});
});

it('should produce both positive and negative variations across different seeds', () => {
// Use a large set of seeds to statistically ensure we get both positive and negative variations
const seeds = Array.from({ length: 100 }, (_, i) => `seed-${i}`);

const results = seeds.map((seed) =>
applyDeterministicVariation({
value: baseValue,
seed,
maxVariationPercent,
}),
);

const hasPositiveVariation = results.some((r) => r > baseValue);
const hasNegativeVariation = results.some((r) => r < baseValue);

expect(hasPositiveVariation).toBe(true);
expect(hasNegativeVariation).toBe(true);
});

it('should return an integer (rounded result)', () => {
const result = applyDeterministicVariation({
value: 1000,
seed: 'test-seed',
maxVariationPercent: 7.6,
});

expect(Number.isInteger(result)).toBe(true);
});

it('should scale variation based on maxVariationPercent', () => {
const seed = 'consistent-seed';

const result = applyDeterministicVariation({
value: baseValue,
seed,
maxVariationPercent: 15,
});

// The deviation from base should be proportionally larger with higher maxVariationPercent
const deviation = Math.abs(result - baseValue);

expect(deviation).toBe(34); // Expected deviation for 15% max variation
});

it('should handle zero value', () => {
const result = applyDeterministicVariation({
value: 0,
seed: 'any-seed',
maxVariationPercent: 7.6,
});

expect(result).toBe(0);
});

it('should return original value when seed is empty', () => {
const result = applyDeterministicVariation({
value: baseValue,
seed: '',
maxVariationPercent: 7.6,
});

// Empty seed should return original value without variation
expect(result).toBe(baseValue);
});
});
16 changes: 8 additions & 8 deletions __tests__/schema/opportunity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6037,7 +6037,7 @@ describe('query opportunityPreview', () => {
anonUserId: 'test-anon-user-123',
preview: {
userIds: ['1', '2'],
totalCount: 2,
totalCount: 2000, // mocked total count
},
},
},
Expand All @@ -6053,7 +6053,7 @@ describe('query opportunityPreview', () => {
expect(res.data.opportunityPreview.result.opportunityId).toBe(
opportunitiesFixture[0].id,
);
expect(res.data.opportunityPreview.result.totalCount).toBe(2);
expect(res.data.opportunityPreview.result.totalCount).toBe(2122);
});

it('should return opportunity preview result structure', async () => {
Expand All @@ -6064,7 +6064,7 @@ describe('query opportunityPreview', () => {
anonUserId: 'test-anon-user-123',
preview: {
userIds: ['1'],
totalCount: 1,
totalCount: 1000, // mocked total count
},
},
},
Expand All @@ -6078,7 +6078,7 @@ describe('query opportunityPreview', () => {
const result = res.data.opportunityPreview.result;
expect(result).toMatchObject({
opportunityId: opportunitiesFixture[0].id,
totalCount: 1,
totalCount: 1061,
tags: expect.any(Array),
companies: expect.any(Array),
squads: expect.any(Array),
Expand All @@ -6095,7 +6095,7 @@ describe('query opportunityPreview', () => {
flags: {
preview: {
userIds: ['1', '2'],
totalCount: 2,
totalCount: 2000, // mocked total count
status: OpportunityPreviewStatus.READY,
},
},
Expand All @@ -6117,7 +6117,7 @@ describe('query opportunityPreview', () => {
expect(res.data.opportunityPreview.result.opportunityId).toBe(
opportunitiesFixture[0].id,
);
expect(res.data.opportunityPreview.result.totalCount).toBe(2);
expect(res.data.opportunityPreview.result.totalCount).toBe(2122);
expect(res.data.opportunityPreview.result.status).toBe(
OpportunityPreviewStatus.READY,
);
Expand Down Expand Up @@ -6189,7 +6189,7 @@ describe('query opportunityPreview', () => {
anonUserId: 'test-anon-user-123',
preview: {
userIds: ['1'],
totalCount: 1,
totalCount: 1000, // mocked total count
},
},
},
Expand All @@ -6203,7 +6203,7 @@ describe('query opportunityPreview', () => {
const result = res.data.opportunityPreview.result;
expect(result).toMatchObject({
opportunityId: opportunitiesFixture[0].id,
totalCount: 1,
totalCount: 1061,
tags: expect.any(Array),
companies: expect.any(Array),
squads: expect.any(Array),
Expand Down
39 changes: 39 additions & 0 deletions src/common/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,42 @@ export const formatMetricValue = (value: number): string | null => {
minimumFractionDigits: 0,
});
};

/**
* Applies a deterministic percentage variation to a value based on a seed string.
* The same seed will always produce the same variation.
* Uses djb2 hash algorithm for consistent hashing.
*
* @param value - The original value
* @param seed - A string to seed the variation (e.g., opportunity ID)
* @param maxVariationPercent - Maximum variation percentage (e.g., 7.6 for ±7.6%)
* @returns The value with deterministic variation applied, rounded to integer
*/
export const applyDeterministicVariation = ({
value,
seed,
maxVariationPercent,
}: {
value: number;
seed: string;
maxVariationPercent: number;
}): number => {
if (!seed) {
return value;
}

// djb2 hash algorithm (produces 32-bit signed integers)
let hash = 0;
for (let i = 0; i < seed.length; i++) {
hash = (hash << 5) - hash + seed.charCodeAt(i);
hash = hash & hash;
}

// Normalize hash to range [-1, 1] using the full 32-bit signed integer range
// 2^31 = 2147483648, so dividing gives us approximately [-1, 1]
const normalizedHash = hash / 2147483648;

const variationFactor = 1 + normalizedHash * (maxVariationPercent / 100);

return Math.round(value * variationFactor);
};
7 changes: 6 additions & 1 deletion src/schema/opportunity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import {
import { createOpportunityPrompt } from '../common/opportunity/prompt';
import { queryPaginatedByDate } from '../common/datePageGenerator';
import { queryReadReplica } from '../common/queryReadReplica';
import { applyDeterministicVariation } from '../common/number';
import { ConnectionArguments } from 'graphql-relay';
import { ProfileResponse, snotraClient } from '../integrations/snotra';
import { slackClient } from '../common/slack';
Expand Down Expand Up @@ -1681,7 +1682,11 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
tags,
companies,
squads,
totalCount: opportunityPreview.totalCount,
totalCount: applyDeterministicVariation({
value: opportunityPreview.totalCount,
seed: opportunity.id,
maxVariationPercent: 7.6,
}),
status: opportunityPreview.status,
opportunityId: opportunity.id,
},
Expand Down
Loading