Skip to content

Commit

Permalink
feat: set addtl custom attribute via logging service; make requests t…
Browse files Browse the repository at this point in the history
…o BFF API endpoints with logError/logInfo (#1234)
  • Loading branch information
adamstankiewicz authored Dec 10, 2024
1 parent 534156c commit cb3d2bf
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 61 deletions.
38 changes: 0 additions & 38 deletions src/components/app/data/services/bffs.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { logError, logInfo } from '@edx/frontend-platform/logging';

import { v4 as uuidv4 } from 'uuid';
import { camelCaseObject } from '@edx/frontend-platform';
Expand All @@ -13,23 +14,29 @@ getAuthenticatedHttpClient.mockReturnValue(axios);
const APP_CONFIG = {
ENTERPRISE_ACCESS_BASE_URL: 'http://localhost:18270',
};

jest.mock('@edx/frontend-platform/config', () => ({
...jest.requireActual('@edx/frontend-platform'),
getConfig: jest.fn(() => APP_CONFIG),
}));

jest.mock('@edx/frontend-platform/auth', () => ({
...jest.requireActual('@edx/frontend-platform/auth'),
getAuthenticatedHttpClient: jest.fn(),
}));
jest.mock('@edx/frontend-platform/logging', () => ({
...jest.requireActual('@edx/frontend-platform/logging'),
logError: jest.fn(),
logInfo: jest.fn(),
}));

const mockEnterpriseCustomer = enterpriseCustomerFactory();
const mockCustomerAgreementUuid = uuidv4();
const mockSubscriptionCatalogUuid = uuidv4();
const mockSubscriptionLicenseUuid = uuidv4();
const mockSubscriptionPlanUuid = uuidv4();
const mockActivationKey = uuidv4();
const mockBFFDashboardResponse = {

const mockBaseLearnerBFFResponse = {
enterprise_customer_user_subsidies: {
subscriptions: {
customer_agreement: {
Expand Down Expand Up @@ -96,6 +103,12 @@ const mockBFFDashboardResponse = {
},
},
},
errors: [],
warnings: [],
};

const mockBFFDashboardResponse = {
...mockBaseLearnerBFFResponse,
enterprise_course_enrollments: [
{
course_run_id: 'course-v1:edX+DemoX+3T2022',
Expand All @@ -119,25 +132,82 @@ const mockBFFDashboardResponse = {
is_revoked: false,
},
],
errors: [],
warnings: [],
};

describe('fetchEnterpriseLearnerDashboard', () => {
const enterpriseDashboard = `${APP_CONFIG.ENTERPRISE_ACCESS_BASE_URL}/api/v1/bffs/learner/dashboard/`;
const urlForDashboardBFF = `${APP_CONFIG.ENTERPRISE_ACCESS_BASE_URL}/api/v1/bffs/learner/dashboard/`;

beforeEach(() => {
jest.clearAllMocks();
axiosMock.reset();
});

it('returns learner dashboard metadata', async () => {
axiosMock.onPost(enterpriseDashboard).reply(200, mockBFFDashboardResponse);
const result = await fetchEnterpriseLearnerDashboard({ enterpriseId: mockEnterpriseCustomer.uuid });
it.each([
{
enterpriseId: mockEnterpriseCustomer.uuid,
enterpriseSlug: undefined,
},
{
enterpriseId: undefined,
enterpriseSlug: mockEnterpriseCustomer.slug,
},
{
enterpriseId: mockEnterpriseCustomer.uuid,
enterpriseSlug: mockEnterpriseCustomer.slug,
},
])('returns learner dashboard metadata (%s)', async ({
enterpriseId,
enterpriseSlug,
}) => {
axiosMock.onPost(urlForDashboardBFF).reply(200, mockBFFDashboardResponse);
const result = await fetchEnterpriseLearnerDashboard({ enterpriseId, enterpriseSlug });
expect(result).toEqual(camelCaseObject(mockBFFDashboardResponse));
});

it('catches error and returns null', async () => {
axiosMock.onPost(enterpriseDashboard).reply(404, learnerDashboardBFFResponse);
const result = await fetchEnterpriseLearnerDashboard(null);
it.each([
{
enterpriseId: mockEnterpriseCustomer.uuid,
enterpriseSlug: undefined,
},
{
enterpriseId: undefined,
enterpriseSlug: mockEnterpriseCustomer.slug,
},
{
enterpriseId: mockEnterpriseCustomer.uuid,
enterpriseSlug: mockEnterpriseCustomer.slug,
},
{
enterpriseId: undefined,
enterpriseSlug: undefined,
},
])('catches error and returns default dashboard BFF response (%s)', async ({
enterpriseId,
enterpriseSlug,
}) => {
axiosMock.onPost(urlForDashboardBFF).reply(404, learnerDashboardBFFResponse);
const result = await fetchEnterpriseLearnerDashboard({ enterpriseId, enterpriseSlug });
expect(result).toEqual(learnerDashboardBFFResponse);
});

it('logs errors and warnings from BFF response', async () => {
const mockError = {
developer_message: 'This is a developer message',
};
const mockWarning = {
developer_message: 'This is a developer warning',
};
const mockResponseWithErrorsAndWarnings = {
...mockBFFDashboardResponse,
errors: [mockError],
warnings: [mockWarning],
};
axiosMock.onPost(urlForDashboardBFF).reply(200, mockResponseWithErrorsAndWarnings);
const result = await fetchEnterpriseLearnerDashboard({ enterpriseSlug: mockEnterpriseCustomer.slug });
expect(result).toEqual(camelCaseObject(mockResponseWithErrorsAndWarnings));

// Assert the logError and logInfo functions were called with the expected arguments.
expect(logError).toHaveBeenCalledWith(`BFF Error (${urlForDashboardBFF}): ${mockError.developer_message}`);
expect(logInfo).toHaveBeenCalledWith(`BFF Warning (${urlForDashboardBFF}): ${mockWarning.developer_message}`);
});
});
111 changes: 111 additions & 0 deletions src/components/app/data/services/bffs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { getConfig } from '@edx/frontend-platform/config';
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { camelCaseObject, snakeCaseObject } from '@edx/frontend-platform/utils';

export const baseLearnerBFFResponse = {
enterpriseCustomerUserSubsidies: {
subscriptions: {
customerAgreement: {},
subscriptionLicenses: [],
subscriptionLicensesByStatus: {},
},
},
errors: [],
warnings: [],
};

export const learnerDashboardBFFResponse = {
...baseLearnerBFFResponse,
enterpriseCourseEnrollments: [],
};

/**
* Log any errors and warnings from the BFF response.
* @param {Object} args
* @param {String} args.url - The URL of the BFF API endpoint.
* @param {Object} args.response - The camelCased response from the BFF API endpoint.
*/
export function logErrorsAndWarningsFromBFFResponse({ url, response }) {
response.errors.forEach((error) => {
logError(`BFF Error (${url}): ${error.developerMessage}`);
});
response.warnings.forEach((warning) => {
logInfo(`BFF Warning (${url}): ${warning.developerMessage}`);
});
}

/**
* Make a request to the specified BFF API endpoint.
* @param {Object} args
* @param {String} args.url - The URL of the BFF API endpoint.
* @param {Object} args.defaultResponse - The default response to return if unable to resolve the request.
* @param {Object} args.options - The options to pass to the BFF API endpoint.
* @param {String} [args.options.enterpriseId] - The UUID of the enterprise customer.
* @param {String} [args.options.enterpriseSlug] - The slug of the enterprise customer.
* @returns {Promise<Object>} - The response from the BFF.
*/
export async function makeBFFRequest({
url,
defaultResponse,
options = {} as Types.BFFRequestOptions,
}) {
const { enterpriseId, enterpriseSlug, ...optionsRest } = options;
const snakeCaseOptionsRest = optionsRest ? snakeCaseObject(optionsRest) : {};

// If neither enterpriseId or enterpriseSlug is provided, return the default response.
if (!enterpriseId && !enterpriseSlug) {
return defaultResponse;
}

try {
const params = {
enterprise_customer_uuid: enterpriseId,
enterprise_customer_slug: enterpriseSlug,
...snakeCaseOptionsRest,
};

// Make request to BFF.
const result = await getAuthenticatedHttpClient().post(url, params);
const response = camelCaseObject(result.data);

// Log any errors and warnings from the BFF response.
logErrorsAndWarningsFromBFFResponse({ url, response });

// Return the response from the BFF.
return response;
} catch (error) {
logError(error);
return defaultResponse;
}
}

export interface EnterpriseLearnerDashboardOptions {
enterpriseId?: string;
enterpriseSlug?: string;
}

/**
* Fetch the learner dashboard BFF API for the specified enterprise customer.
* @param {Object} args
* @param {String} [args.enterpriseId] - The UUID of the enterprise customer.
* @param {String} [args.enterpriseSlug] - The slug of the enterprise customer.
* @returns {Promise<Object>} - The learner dashboard metadata.
*/
export async function fetchEnterpriseLearnerDashboard({
enterpriseId,
enterpriseSlug,
}: EnterpriseLearnerDashboardOptions) {
const options = {} as Types.BFFRequestOptions;
if (enterpriseId) {
options.enterpriseId = enterpriseId;
}
if (enterpriseSlug) {
options.enterpriseSlug = enterpriseSlug;
}
return makeBFFRequest({
url: `${getConfig().ENTERPRISE_ACCESS_BASE_URL}/api/v1/bffs/learner/dashboard/`,
defaultResponse: learnerDashboardBFFResponse,
options,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Factory.define('enterpriseCustomer')
secondary_color: faker.internet.color(),
tertiary_color: faker.internet.color(),
});
export function enterpriseCustomerFactory(overrides = {}) {
export function enterpriseCustomerFactory(overrides = {}): Types.EnterpriseCustomer {
return camelCaseObject(Factory.build('enterpriseCustomer', overrides));
}

Expand Down
28 changes: 21 additions & 7 deletions src/components/enterprise-page/EnterprisePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,37 @@ import { getLoggingService } from '@edx/frontend-platform/logging';
import { isDefinedAndNotNull } from '../../utils/common';
import { useAlgoliaSearch } from '../../utils/hooks';
import { pushUserCustomerAttributes } from '../../utils/optimizely';
import { useEnterpriseCustomer } from '../app/data';
import { isBFFEnabledForEnterpriseCustomer, useEnterpriseCustomer } from '../app/data';

const EnterprisePage = ({ children }) => {
/**
* Custom hook to set custom attributes for logging service:
* - enterprise_customer_uuid - The UUID of the enterprise customer
* - is_bff_enabled - Whether the BFF is enabled for the enterprise customer
*/
function useLoggingCustomAttributes() {
const { data: enterpriseCustomer } = useEnterpriseCustomer();
const config = getConfig();
const [searchClient, searchIndex] = useAlgoliaSearch(config);
const { authenticatedUser } = useContext(AppContext);

useEffect(() => {
if (isDefinedAndNotNull(enterpriseCustomer)) {
pushUserCustomerAttributes(enterpriseCustomer);

// Set custom attributes for logging service
// Set custom attributes via logging service
const loggingService = getLoggingService();
loggingService.setCustomAttribute('enterprise_customer_uuid', enterpriseCustomer.uuid);
loggingService.setCustomAttribute(
'is_bff_enabled',
isBFFEnabledForEnterpriseCustomer(enterpriseCustomer.uuid),
);
}
}, [enterpriseCustomer]);
}

const EnterprisePage = ({ children }) => {
const config = getConfig();
const [searchClient, searchIndex] = useAlgoliaSearch(config);
const { authenticatedUser } = useContext(AppContext);

// Set custom attributes via logging service
useLoggingCustomAttributes();

const contextValue = useMemo(() => ({
authenticatedUser,
Expand Down
Loading

0 comments on commit cb3d2bf

Please sign in to comment.