radgrad/radgrad2

View on GitHub
archive/issue-415/page-tracking/utilities/page-tracking.ts

Summary

Maintainability
A
0 mins
Test Coverage
import {
  CareerGoal,
  Course,
  Interest, Opportunity,
  PageInterestInfo,
  PageInterestsDailySnapshot, Slug,
} from '../../../../app/imports/typings/radgrad';
import {
  IPageInterestsCategoryTypes,
  PageInterestsCategoryTypes,
} from '../../api/page-tracking/PageInterestsCategoryTypes';
import { CareerGoals } from '../../../../app/imports/api/career/CareerGoalCollection';
import { Courses } from '../../../../app/imports/api/course/CourseCollection';
import { Interests } from '../../../../app/imports/api/interest/InterestCollection';
import { Opportunities } from '../../../../app/imports/api/opportunity/OpportunityCollection';
import { Slugs } from '../../../../app/imports/api/slug/SlugCollection';
import { getLastUrlParam, MatchProps } from '../../../../app/imports/ui/components/shared/utilities/router';
import { EXPLORER_TYPE } from '../../../../app/imports/ui/layouts/utilities/route-constants';

export interface AggregatedDailySnapshot {
  careerGoals: PageInterestInfo[];
  courses: PageInterestInfo[];
  interests: PageInterestInfo[];
  opportunities: PageInterestInfo[];
}

// urlCategory is of type PageInterestsCategoryTypes
export const getUrlCategory = (match: MatchProps) => getLastUrlParam(match) as PageInterestsCategoryTypes;

// category is of type PageInterestsDailySnapshot
export const getCategory = (selectedCategory: IPageInterestsCategoryTypes): string => {
  switch (selectedCategory) {
    case PageInterestsCategoryTypes.CAREERGOAL:
      return 'careerGoals';
    case PageInterestsCategoryTypes.COURSE:
      return 'courses';
    case PageInterestsCategoryTypes.INTEREST:
      return 'interests';
    case PageInterestsCategoryTypes.OPPORTUNITY:
      return 'opportunities';
    default:
      console.error(`Bad selectedCategory: ${selectedCategory}`);
      break;
  }
  return undefined;
};

export const aggregateDailySnapshots = (snapshots: PageInterestsDailySnapshot[]): AggregatedDailySnapshot => {
  const aggregatedSnapshot: AggregatedDailySnapshot = {
    careerGoals: [],
    courses: [],
    interests: [],
    opportunities: [],
  };
  // Arrays that contain areas that have already been iterated through for each corresponding topic category
  const foundCareerGoals = [];
  const foundCourses = [];
  const foundInterests = [];
  const foundOpportunities = [];
  snapshots.forEach((snapshot) => {
    // Career Goals
    snapshot.careerGoals.forEach((careerGoal) => {
      const careerGoalInstance: PageInterestInfo = { name: careerGoal.name, views: careerGoal.views };
      // If we haven't iterated through this career goal, push it to the aggregated snapshot and found career goals.
      if (!containsKey(careerGoal, foundCareerGoals)) {
        foundCareerGoals.push(careerGoalInstance);
        aggregatedSnapshot.careerGoals.push(careerGoalInstance);
        // Otherwise, simply increment the career goal already pushed to the aggregated snapshot with the number of views appropriately
      } else {
        aggregatedSnapshot.careerGoals.filter((cg) => careerGoal.name === cg.name)[0].views += careerGoal.views;
      }
    });
    // Courses
    snapshot.courses.forEach((course) => {
      const courseInstance: PageInterestInfo = { name: course.name, views: course.views };
      if (!containsKey(course, foundCourses)) {
        foundCourses.push(courseInstance);
        aggregatedSnapshot.courses.push(courseInstance);
      } else {
        aggregatedSnapshot.courses.filter((c) => course.name === c.name)[0].views += course.views;
      }
    });
    // Interests
    snapshot.interests.forEach((interest) => {
      const interestInstance: PageInterestInfo = { name: interest.name, views: interest.views };
      if (!containsKey(interest, foundInterests)) {
        foundInterests.push(interestInstance);
        aggregatedSnapshot.interests.push(interestInstance);
      } else {
        aggregatedSnapshot.interests.filter((i) => interest.name === i.name)[0].views += interest.views;
      }
    });
    // Opportunities
    snapshot.opportunities.forEach((opportunity) => {
      const opportunityInstance: PageInterestInfo = { name: opportunity.name, views: opportunity.views };
      if (!containsKey(opportunity, foundOpportunities)) {
        foundOpportunities.push(opportunityInstance);
        aggregatedSnapshot.opportunities.push(opportunityInstance);
      } else {
        aggregatedSnapshot.opportunities.filter((opp) => opportunity.name === opp.name)[0].views += opportunity.views;
      }
    });
  });
  return aggregatedSnapshot;
};

const containsKey = (object: PageInterestInfo, arr: PageInterestInfo[]) => {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i].name === object.name) {
      return true;
    }
  }
  return false;
};

export const parseName = (category: IPageInterestsCategoryTypes, slug: string): string => {
  let doc: (CareerGoal | Course | Interest | Opportunity);
  switch (category) {
    case PageInterestsCategoryTypes.CAREERGOAL:
      doc = CareerGoals.findDocBySlug(slug);
      break;
    case PageInterestsCategoryTypes.COURSE:
      doc = Courses.findDocBySlug(slug);
      break;
    case PageInterestsCategoryTypes.INTEREST:
      doc = Interests.findDocBySlug(slug);
      break;
    case PageInterestsCategoryTypes.OPPORTUNITY:
      doc = Opportunities.findDocBySlug(slug);
      break;
    default:
      console.error(`Bad category: ${category}`);
      break;
  }
  return doc.name;
};

export const slugIDToSlugName = (slugID) => Slugs.findOne(slugID).name;

// Calculates how long to wait (since the time the student has opened the page) before they're considered "interested" in that item
export const calculateEngagedInterestTime = (slugName: string): number => {
  const slug: Slug = Slugs.findOne({ name: slugName });
  const type = slug.entityName;
  const descriptionText = getDescriptionText(type, slugName);

  const descriptionTextStrings: string[] = descriptionText.split(' ');
  const removedHttpLinks: string[] = descriptionTextStrings.filter((str) => !str.includes('http')); // Remove the Markdown http links
  const validWordLength = 3; // Number of characters for a string to be considered a "word"
  const validDescriptionTextStrings: string[] = removedHttpLinks.filter((str) => str.length >= validWordLength); // Remove strings that aren't "valid words"
  const isLongDescriptionText: boolean = validDescriptionTextStrings.length > 50; // If there are more than 50 words, it's considered a long description

  const wpm = isLongDescriptionText ? 275 : 250; // Words per minute. We expect that the longer a description is, the faster a english-literate college student reads
  // We expect that in order for a student to have been considered "interested" in the item they're reading
  // that they have read around half of the words in the description
  const expectedNumberOfWords = validDescriptionTextStrings.length / 2;
  const estimatedReadingTime = (expectedNumberOfWords / wpm) * 60 * 1000; // Based on the WPM, the estimated time it would take to read half of the description (in milliseconds)
  // As reading experience and time varies between students (in the sense that they might not read everything word by word),
  // we only expect that they have spent a minimum of half of the estimated reading time for reading half of the description
  // to be considered "interested" in that item
  const expectedReadingTime = estimatedReadingTime / 2;

  // Students may not necessarily start reading the description as soon as they enter the page.
  // If it is a longer description, they might spend some time skimming the page first before reading. (in milliseconds)
  const initiationTime = isLongDescriptionText ? 1500 : 500;
  return expectedReadingTime + initiationTime;
};

// Checks if the URL parameter is one of the explorer types that we're tracking
// Valid explorer types that we track are Career Goals, Courses, Interests, and Opportunities
export const isValidParameter = (parameter) => (parameter === EXPLORER_TYPE.CAREERGOALS || parameter === EXPLORER_TYPE.COURSES || parameter === EXPLORER_TYPE.INTERESTS || parameter === EXPLORER_TYPE.OPPORTUNITIES);

export const getDescriptionText = (type, slugName) => {
  const id = Slugs.findOne({ name: slugName })._id;
  switch (type) { // entityNames are the types
    case CareerGoals.getType():
      return CareerGoals.findOne({ slugID: id }).description;
    case Courses.getType():
      return Courses.findOne({ slugID: id }).description;
    case Interests.getType():
      return Interests.findOne({ slugID: id }).description;
    case Opportunities.getType():
      return Opportunities.findOne({ slugID: id }).description;
    default:
      console.error(`Bad entityName: ${type}`);
      break;
  }
  return undefined;
};