Skip to content

Commit

Permalink
Merge pull request #3344 from MasterHW/oso
Browse files Browse the repository at this point in the history
Add OpenSourceObserver stats to explorer
  • Loading branch information
MasterHW authored Apr 30, 2024
2 parents 288e3b9 + b60de9a commit 62bace8
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 16 deletions.
5 changes: 4 additions & 1 deletion packages/grant-explorer/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,7 @@ REACT_APP_EXPLORER_DISABLE_MATCHING_ESTIMATES=false
REACT_APP_PASSPORT_API_KEY="test-key"
REACT_APP_PASSPORT_AVALANCHE_API_KEY="test-key"
REACT_APP_PASSPORT_API_COMMUNITY_ID="0000"
REACT_APP_PASSPORT_API_COMMUNITY_ID_AVALANCHE="0000"
REACT_APP_PASSPORT_API_COMMUNITY_ID_AVALANCHE="0000"

# create key following https://www.opensource.observer/docs/get-started/
REACT_APP_OSO_API_KEY=
2 changes: 2 additions & 0 deletions packages/grant-explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
"ethereum-blockies": "^0.1.1",
"ethers": "^5.7.2",
"framer-motion": "^10.15.0",
"graphql": "^16.8.1",
"graphql-request": "^6.1.0",
"history": "^5.3.0",
"html-react-parser": "^3.0.7",
"https-browserify": "^1.0.0",
Expand Down
175 changes: 175 additions & 0 deletions packages/grant-explorer/src/features/api/oso.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { useState } from "react";
import useSWR from "swr";
import { Hex } from "viem";
import { gql, GraphQLClient } from "graphql-request";

const osoApiKey = process.env.REACT_APP_OSO_API_KEY as string;
const osoUrl = "https://opensource-observer.hasura.app/v1/graphql";
const graphQLClient = new GraphQLClient(osoUrl, {
headers: {
authorization: `Bearer ${osoApiKey}`,
},
});
let hasFetched = false;

interface IOSOId {
artifacts_by_project: {
project_id: Hex;
};
}

export interface IOSOStats {
code_metrics_by_project: {
contributors: number;
first_commit_date: number;
};
events_monthly_to_project: [
{
bucket_month: number;
amount: number;
},
{
bucket_month: number;
amount: number;
},
{
bucket_month: number;
amount: number;
},
{
bucket_month: number;
amount: number;
},
{
bucket_month: number;
amount: number;
},
{
bucket_month: number;
amount: number;
},
];
}

export function useOSO(projectGithub?: string) {
const emptyReturn: IOSOStats = {
code_metrics_by_project: {
contributors: 0,
first_commit_date: 0,
},
events_monthly_to_project: [
{
bucket_month: 0,
amount: 0,
},
{
bucket_month: 0,
amount: 0,
},
{
bucket_month: 0,
amount: 0,
},
{
bucket_month: 0,
amount: 0,
},
{
bucket_month: 0,
amount: 0,
},
{
bucket_month: 0,
amount: 0,
},
],
};
const [stats, setStats] = useState<IOSOStats | null>(null);

const getStatsFor = async (projectRegistryGithub: string) => {
if (osoApiKey === "")
throw new Error("OpenSourceObserver API key not set.");
const queryId = gql`{
artifacts_by_project(where: {artifact_name: {_ilike: "%${projectRegistryGithub}/%"}}
distinct_on: project_id
) {
project_id
}
}`;

try {
hasFetched = true;
const idData: IOSOId = await graphQLClient.request<IOSOId>(queryId);

if (!Array.isArray(idData.artifacts_by_project)) {
setStats(emptyReturn);
return;
}

const parsedId: IOSOId = {
artifacts_by_project: idData.artifacts_by_project[0],
};

const queryStats = gql`{
code_metrics_by_project(where: {project_id: {_eq: "${parsedId.artifacts_by_project.project_id}"}}) {
contributors
first_commit_date
}
events_monthly_to_project(
where: {project_id: {_eq: "${parsedId.artifacts_by_project.project_id}"}, event_type: {_eq: "COMMIT_CODE"}}
limit: 6
order_by: {bucket_month: desc}
) {
bucket_month
amount
}
}`;

const items: IOSOStats =
await graphQLClient.request<IOSOStats>(queryStats);

if (!Array.isArray(items.code_metrics_by_project)) {
setStats(emptyReturn);
return;
}

if (items.events_monthly_to_project.length === 6) {
const parsedItems: IOSOStats = {
code_metrics_by_project: items.code_metrics_by_project[0],
events_monthly_to_project: items.events_monthly_to_project,
};
setStats(parsedItems);
} else {
const parsedItems: IOSOStats = {
code_metrics_by_project: items.code_metrics_by_project[0],
events_monthly_to_project: emptyReturn.events_monthly_to_project,
};
setStats(parsedItems);
}
} catch (e) {
console.error(`No stats found for project: ${projectGithub}`);
console.error(e);
setStats(emptyReturn);
}
};

const { isLoading } = useSWR(osoUrl, {
fetcher: async () => projectGithub && getStatsFor(projectGithub),
revalidateOnMount: true,
});

if (stats === null && !hasFetched)
projectGithub && getStatsFor(projectGithub);
return {
/**
* Fetch OSO for stats on a project
* @param projectRegistryGithub projectGithub
*/
getStatsFor,
/**
* Stats for a project (loaded from OSO)
*/
stats,
isStatsLoading: isLoading,
};
}
15 changes: 15 additions & 0 deletions packages/grant-explorer/src/features/common/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,18 @@ export const getFormattedRoundId = (roundId?: string | number): string => {
return roundId;
}
};

export function formatTimeAgo(dateString : number) {
const date = new Date(dateString);
const now = new Date();
const diffTime = Math.abs(now.getTime() - date.getTime()); // Difference in milliseconds
const diffMonths = Math.round(diffTime / (1000 * 60 * 60 * 24 * 30)); // Convert to months

if (diffMonths === 0) {
return 'This month';
} else if (diffMonths === 1) {
return 'Last month';
} else {
return `${diffMonths} months`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface GrantListProps {
export const GrantList: React.FC<GrantListProps> = ({ grants }) => {
return (
<Flex gap={2} flexDir="column" py={6} px={3}>
<h4 className="text-3xl">Project milestones</h4>
{grants.length > 0 ? (
<>
<Text className="text-[18px]">Total grants ({grants.length})</Text>
Expand All @@ -20,7 +21,7 @@ export const GrantList: React.FC<GrantListProps> = ({ grants }) => {
url={getGapProjectUrl(grant.projectUID, grant.uid)}
/>
))}
<Text fontFamily="DM Mono" textAlign="center">
<Text fontFamily="DM Mono" textAlign="center" className={"text-xs"}>
Data provided by Karma via{" "}
<Link href={gapAppUrl} target="_blank">
<Text as="span" className="text-gitcoin-violet-500">
Expand Down
77 changes: 77 additions & 0 deletions packages/grant-explorer/src/features/round/OSO/ImpactStats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from "react";
import { IOSOStats } from "../../api/oso";
import { Flex, Link, Text } from "@chakra-ui/react";
import { Stat } from "../ViewProjectDetails";
import { formatTimeAgo } from "../../common/utils/utils";


export const StatList = ({ stats }: { stats: IOSOStats | null }) => {
if (stats === null) return;
return (
stats.code_metrics_by_project.contributors > 0 ? (
<React.Fragment>
<h4 className="text-3xl mt-5 ml-4" >Impact stats</h4>
<Flex gap={2} flexDir={{base: 'column', md: 'row'}} py={6} px={3} >
<div
className={
"rounded-2xl bg-gray-50 flex-auto p-3 md:p-6 gap-4 flex flex-col"
}
>
<div> <Stat
isLoading={false}
value={`${formatTimeAgo(stats.code_metrics_by_project.first_commit_date)}`}
>
Project age
</Stat>
</div>
</div>
<div
className={
"rounded-2xl bg-gray-50 flex-auto p-3 md:p-6 gap-4 flex flex-col"
}
>
<Stat
isLoading={false}
value={`${stats.code_metrics_by_project.contributors}`}
>
Unique code contributors
</Stat>
</div>
<div
className={
"rounded-2xl bg-gray-50 flex-auto p-3 md:p-6 gap-4 flex flex-col"
}
>
<Stat
isLoading={false}
value={`${projectVelocity(stats)}`}
>
Velocity
</Stat>
</div>
</Flex>
<Text fontFamily="DM Mono" textAlign="center" mt={0} className={"text-xs"}>
Data provided by {" "}
<Link href={"https://www.opensource.observer/"} target="_blank">
<Text as="span" className="text-gitcoin-violet-500">
opensource.observer
</Text>
</Link>
</Text>
</React.Fragment>
) : (
<div>
</div>
)
);
};

function projectVelocity(stats : IOSOStats) {
const recentCommits = stats.events_monthly_to_project[0].amount + stats.events_monthly_to_project[1].amount + stats.events_monthly_to_project[2].amount;
const olderCommits = stats.events_monthly_to_project[3].amount + stats.events_monthly_to_project[4].amount + stats.events_monthly_to_project[5].amount;

if (recentCommits === 0 && olderCommits === 0) return 'unknown';
if (recentCommits >= (1.5 * olderCommits)) return 'increasing';
if (recentCommits <= 0.5 * olderCommits) return 'decreasing';
return 'steady';
}
17 changes: 12 additions & 5 deletions packages/grant-explorer/src/features/round/ViewProjectDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import { useCartStorage } from "../../store";
import { Box, Skeleton, SkeletonText, Tab, Tabs } from "@chakra-ui/react";
import { GrantList } from "./KarmaGrant/GrantList";
import { useGap } from "../api/gap";
import { StatList } from "./OSO/ImpactStats";
import { useOSO } from "../api/oso";
import { CheckIcon, ShoppingCartIcon } from "@heroicons/react/24/outline";
import { DataLayer, useDataLayer } from "data-layer";
import { DefaultLayout } from "../common/DefaultLayout";
Expand Down Expand Up @@ -120,6 +122,7 @@ export default function ViewProjectDetails() {
round?.roundMetadata?.quadraticFundingConfig?.sybilDefense === true;

const { grants } = useGap(projectToRender?.projectRegistryId as string);
const { stats } = useOSO(projectToRender?.projectMetadata.projectGithub as string);

const currentTime = new Date();
const isAfterRoundEndDate =
Expand Down Expand Up @@ -178,7 +181,6 @@ export default function ViewProjectDetails() {
const {
projectMetadata: { title, description = "", bannerImg },
} = projectToRender ?? { projectMetadata: {} };

const projectDetailsTabs = useMemo(
() => [
{
Expand All @@ -202,11 +204,16 @@ export default function ViewProjectDetails() {
),
},
{
name: "Milestone updates",
content: <GrantList grants={grants} />,
name: "Impact Measurement",
content: (
<React.Fragment>
<StatList stats={stats} />
<GrantList grants={grants} />
</React.Fragment>
),
},
],
[grants, projectToRender, description]
[stats, grants, projectToRender, description]
);

const handleTabChange = (tabIndex: number) => {
Expand Down Expand Up @@ -584,7 +591,7 @@ export function ProjectStats() {
);
}

function Stat({
export function Stat({
value,
children,
isLoading,
Expand Down
Loading

0 comments on commit 62bace8

Please sign in to comment.