Skip to content
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/node_modules
/.pnp
.pnp.js
/api-go/tools/puppeteer/node_modules

# testing
/coverage
Expand Down
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,53 @@ We maintain a [list](api-go/config/multi-repo.md) which you could directly add y

[![Monthly Active Contributors](https://contributor-overtime-api.apiseven.com/contributors-svg?chart=contributorMonthlyActivity&repo=apache/apisix&merge=true)](https://www.apiseven.com/en/contributor-graph?chart=contributorMonthlyActivity&repo=apache/apisix&merge=true)

## Development
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since normal contributors could not get access by default, so maybe we could move it to a separate file and leave a link here, so not all people need to see it on README.

Also maybe we could also add guidance on how to get access.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, we do need a guide to tell a new developer how to start this project.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's pretty complicated, since we deploy it on something we pay for, we need to find the way to give enough permission for users to contribute, but also avoid people abusing it


The current project uses Google Cloud deployment, to develop and debug locally, follow the steps below:

This project depends on `Golang` and `Node.js`, please make sure you have the corresponding environment.

Because the read and write functions of Google Clould DataStore and Google Clould DataStorage are used in the project, you need to obtain the corresponding permissions of this.

It is recommended to download the key file of Google Clould in json format, and then set the environment variable locally:

```
GOOGLE_APPLICATION_CREDENTIALS=/The/path/of/json/key/file
```

1. Clone the project.

```
git clone https://github.com/api7/contributor-graph.git
```

2. Start the front end.

```
cd contributor-graph
yarn
yarn dev
```

3. Start API Server.

```
cd api-go
go run ./cmd/contributor/main.go
```

4. Start Google Cloud Function locally.

This step depends on [@google-cloud/functions-framework](https://www.npmjs.com/package/@google-cloud/functions-framework) tool, please install the tool first.

```
cd ./api-go/tools/puppeteer
yarn
npx @google-cloud/functions-framework --target=png
```

Then config this address as Clould Function Trigger link in `api-go`.

## Feature request

If you have any requests, including but not limited to:
Expand Down
26 changes: 22 additions & 4 deletions api-go/cmd/contributor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log"
Expand Down Expand Up @@ -130,11 +131,28 @@ func getContributorSVG(w http.ResponseWriter, r *http.Request) {
return
}
}
w.Header().Add("content-type", "image/svg+xml;charset=utf-8")
w.Header().Add("cache-control", "public, max-age=86400")

svg = strings.Replace(svg, "%", "%%", -1)
fmt.Fprintf(w, svg)
if strings.Contains(svg, "svg") {
w.Header().Add("content-type", "image/svg+xml;charset=utf-8")
w.Header().Add("cache-control", "public, max-age=86400")

svg = strings.Replace(svg, "%", "%%", -1)
fmt.Fprintf(w, svg)
} else {
w.Header().Add("content-type", "image/png")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we choose svg but not png before is because svg is relative small (at least 5 times smaller I believe).
Is there any reason to change to png

Copy link
Copy Markdown
Contributor Author

@Baoyuantop Baoyuantop Aug 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can only produce png when I render the picture in Google Cloud Function. Any better suggestions? thanks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe Echarts support both png and svg. I think @LiteSun know more about it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SVG is better, let me check the method again, @LiteSun do you have any good suggestions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

svg would be beteer. see:
image

w.Header().Add("cache-control", "public, max-age=86400")

base64String := strings.Split(svg, "data:image/png;base64,")[1]
buffer, err := base64.StdEncoding.DecodeString(base64String)

if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(err.Error())
return
}

w.Write(buffer)
}
}

func getRepos(w http.ResponseWriter, r *http.Request) {
Expand Down
112 changes: 55 additions & 57 deletions api-go/internal/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"io"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"

"cloud.google.com/go/datastore"
Expand All @@ -29,7 +27,7 @@ func GenerateAndSaveSVG(ctx context.Context, repo string, merge bool, chartType
}
defer client.Close()

graphFunctionUrl := "https://cloudfunction.contributor-graph.com/svg?repo=" + repo
graphFunctionUrl := "https://asia-east2-api7-301102.cloudfunctions.net/png" + repo
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the original url, which is actually the local balancer URL of the cloud function which adds google cloud armor

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t know much about Google Cloud, I just want to point it to my newly deployed function.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could bind your new function to the load balancer and there are docs talking about it

if merge {
graphFunctionUrl += "&merge=true"
}
Expand Down Expand Up @@ -81,7 +79,7 @@ func GenerateAndSaveSVG(ctx context.Context, repo string, merge bool, chartType

wc := client.Bucket(bucket).Object(object).NewWriter(ctx)
wc.CacheControl = "public, max-age=86400"
wc.ContentType = "image/svg+xml;charset=utf-8"
wc.ContentType = "image/png"

if _, err = io.Copy(wc, bytes.NewReader(svg)); err != nil {
return "", fmt.Errorf("upload svg failed: io.Copy: %v", err)
Expand Down Expand Up @@ -161,58 +159,58 @@ func SubGetSVG(w http.ResponseWriter, repo string, merge bool, charType string)
// we need to also tell if the graph is ready to use on this side.
// Try to get the endpoint of the line drawn and tell if it's on the right-most side
func svgSucceed(svgBytes []byte) ([]byte, error) {
svg := string(svgBytes[:])
lines := strings.Split(svg, "\n")
var svgWidth float64
for _, l := range lines {
if strings.Contains(l, "<rect") {
words := strings.Split(l, " ")
for _, w := range words {
if strings.Contains(w, "width") {
parts := strings.Split(w, `"`)
var err error
svgWidth, err = strconv.ParseFloat(parts[1], 64)
if err != nil {
return nil, err
}
break
}
}
}
}
if svgWidth == 0 {
return nil, fmt.Errorf("could not get svg width")
}
lineColor := "39a85a"
for i, l := range lines {
if strings.Contains(l, lineColor) {
lineDrawn := strings.Split(strings.Split(l, `"`)[1], " ")
endPointX, err := strconv.ParseFloat(lineDrawn[len(lineDrawn)-2], 64)
if err != nil {
return nil, err
}
if float64(endPointX) < 0.95*float64(svgWidth) {
return nil, fmt.Errorf("the line is not reach its end")
}
break
}
if i == len(lines)-1 {
return nil, fmt.Errorf("could not get endpoint")
}
}
renderLengthMarker := "<path"
for i := len(lines) - 1; i >= 0; i-- {
if strings.Contains(lines[i], renderLengthMarker) {
words := strings.Split(lines[i], " ")
svgWidthStr := fmt.Sprintf("%f", svgWidth)
for j := range words {
if words[j] == "L" && j+1 < len(words) && words[j+1] != svgWidthStr {
lines[i] = strings.ReplaceAll(lines[i], words[j+1], svgWidthStr)
break
}
}
return []byte(strings.Join(lines, "\n")), nil
}
}
// svg := string(svgBytes[:])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we would like to remove this function, we could directly remove it and use git to look for history version

// lines := strings.Split(svg, "\n")
// var svgWidth float64
// for _, l := range lines {
// if strings.Contains(l, "<rect") {
// words := strings.Split(l, " ")
// for _, w := range words {
// if strings.Contains(w, "width") {
// parts := strings.Split(w, `"`)
// var err error
// svgWidth, err = strconv.ParseFloat(parts[1], 64)
// if err != nil {
// return nil, err
// }
// break
// }
// }
// }
// }
// if svgWidth == 0 {
// return nil, fmt.Errorf("could not get svg width")
// }
// lineColor := "39a85a"
// for i, l := range lines {
// if strings.Contains(l, lineColor) {
// lineDrawn := strings.Split(strings.Split(l, `"`)[1], " ")
// endPointX, err := strconv.ParseFloat(lineDrawn[len(lineDrawn)-2], 64)
// if err != nil {
// return nil, err
// }
// if float64(endPointX) < 0.95*float64(svgWidth) {
// return nil, fmt.Errorf("the line is not reach its end")
// }
// break
// }
// if i == len(lines)-1 {
// return nil, fmt.Errorf("could not get endpoint")
// }
// }
// renderLengthMarker := "<path"
// for i := len(lines) - 1; i >= 0; i-- {
// if strings.Contains(lines[i], renderLengthMarker) {
// words := strings.Split(lines[i], " ")
// svgWidthStr := fmt.Sprintf("%f", svgWidth)
// for j := range words {
// if words[j] == "L" && j+1 < len(words) && words[j+1] != svgWidthStr {
// lines[i] = strings.ReplaceAll(lines[i], words[j+1], svgWidthStr)
// break
// }
// }
// return []byte(strings.Join(lines, "\n")), nil
// }
// }
return svgBytes, nil
}
2 changes: 1 addition & 1 deletion api-go/internal/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func RepoNameToFileName(str string, merge bool, charType string) string {
if charType == ContributorMonthlyActivity {
filename = "monthly/" + filename
}
return filename + ".svg"
return filename + ".png"
}

func FileNameToRepoName(str string) string {
Expand Down
13 changes: 13 additions & 0 deletions api-go/tools/puppeteer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Google Clould Function

Google Clould Function used to generate contributor statistics picture.

## API

The entry point of the function is `png` in `index.js`.

| Parameter | Required | Type | Description | Example |
| ---- | ---- | ---- | ---- | ---- |
| repo | true | string | The name of repository | apache/apisix,apache/skywalking |
| merge | false | boolean | Whether to view all repos related to this repo, when chart is `contributorMonthlyActivity`, can not be set true | true |
| chart | false | contributorOverTime contributorMonthlyActivity | chart type | |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something goes wrong here

123 changes: 123 additions & 0 deletions api-go/tools/puppeteer/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
const moment = require('moment');
const axios = require('axios');

const isSameDay = (d1, d2) => {
return (
d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate()
);
};

const fetchContributorsData = (repo) => {
if (repo === "null" || repo === null) {
repo = "apache/apisix";
}
return new Promise((resolve, reject) => {
axios.get(
`https://contributor-overtime-api.apiseven.com/contributors?repo=${repo}`
).then(response => {
return response.data;
}).then(data => {
const { Contributors = [] } = data;
const sortContributors = Contributors.map(item => ({
...item,
date: item.date.substring(0, 10)
})).sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
if (
!isSameDay(
new Date(sortContributors[sortContributors.length - 1].date),
new Date()
)
) {
sortContributors.push({
repo,
idx: sortContributors[sortContributors.length - 1].idx,
date: moment(new Date()).format("YYYY-MM-DD")
});
};

const processContributors = [];
sortContributors.forEach((item, index) => {
processContributors.push(item);

if (index !== sortContributors.length - 1) {
const diffDays = moment(sortContributors[index + 1].date).diff(
item.date,
"days"
);
if (diffDays > 1) {
for (let index = 1; index < diffDays; index++) {
processContributors.push({
...item,
date: moment(item.date)
.add(index, "days")
.format()
.substring(0, 10)
});
}
}
}
});

const filterData = processContributors.filter(
(item, index) =>
index === 0 ||
index === processContributors.length - 1 ||
new Date(item.date).getDate() % 10 === 5
);

resolve({ repo, ...{ Contributors: filterData } });
}).catch(error => {
reject(error);
})
})
};

const fetchMonthlyData = (repo) => {
if (repo === "null" || repo === null) {
repo = "apache/apisix";
}
return new Promise((resolve, reject) => {
axios.get(
`https://contributor-overtime-api.apiseven.com/monthly-contributor?repo=${repo}`
)
.then(response => {
return response.data;
})
.then(myJson => {
resolve({ repo, ...myJson });
})
.catch(e => {
reject(e);
});
});
};

const fetchMergeContributor = (repo) => {
return new Promise((resolve, reject) => {
axios.get(
`https://contributor-overtime-api.apiseven.com/contributors-multi?repo=${repo.join(
","
)}`
)
.then(response => {
return response.data;
})
.then(myJson => {
console.log('myJson: ', myJson);
resolve({ repo, ...myJson });
})
.catch(e => {
reject(e);
});
});
};

module.exports = {
fetchContributorsData,
fetchMonthlyData,
fetchMergeContributor,
}
Loading