forked from tangled.org/core
this repo has no description

appview: instrument all the things

anirudh.fi df653304 5c8b5c1f

verified
+31 -6
appview/db/issues.go
···
package db
import (
+
"context"
"database/sql"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
+
"go.opentelemetry.io/otel"
+
"go.opentelemetry.io/otel/attribute"
"tangled.sh/tangled.sh/core/appview/pagination"
)
···
return ownerDid, err
}
-
func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) {
+
func GetIssues(ctx context.Context, e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) {
+
ctx, span := otel.Tracer("db").Start(ctx, "GetIssues")
+
defer span.End()
+
+
span.SetAttributes(
+
attribute.String("repo_at", repoAt.String()),
+
attribute.Bool("is_open", isOpen),
+
attribute.Int("page.offset", page.Offset),
+
attribute.Int("page.limit", page.Limit),
+
)
+
var issues []Issue
openValue := 0
if isOpen {
openValue = 1
}
-
rows, err := e.Query(
+
rows, err := e.QueryContext(
+
ctx,
`
with numbered_issue as (
select
···
body,
open,
comment_count
-
from
+
from
numbered_issue
-
where
+
where
row_num between ? and ?`,
repoAt, openValue, page.Offset+1, page.Offset+page.Limit)
if err != nil {
+
span.RecordError(err)
return nil, err
}
defer rows.Close()
···
var metadata IssueMetadata
err := rows.Scan(&issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount)
if err != nil {
+
span.RecordError(err)
return nil, err
}
createdTime, err := time.Parse(time.RFC3339, createdAt)
if err != nil {
+
span.RecordError(err)
return nil, err
}
issue.Created = createdTime
···
}
if err := rows.Err(); err != nil {
+
span.RecordError(err)
return nil, err
}
+
span.SetAttributes(attribute.Int("issues.count", len(issues)))
return issues, nil
}
···
return issues, nil
}
-
func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) {
+
func GetIssue(ctx context.Context, e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) {
+
ctx, span := otel.Tracer("db").Start(ctx, "GetIssue")
+
defer span.End()
+
query := `select owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?`
row := e.QueryRow(query, repoAt, issueId)
···
return &issue, nil
}
-
func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) {
+
func GetIssueWithComments(ctx context.Context, e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) {
+
ctx, span := otel.Tracer("db").Start(ctx, "GetIssueWithComments")
+
defer span.End()
+
query := `select owner_did, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?`
row := e.QueryRow(query, repoAt, issueId)
+29 -3
appview/db/profile.go
···
package db
import (
+
"context"
"fmt"
"time"
+
+
"go.opentelemetry.io/otel/attribute"
+
"go.opentelemetry.io/otel/codes"
+
"go.opentelemetry.io/otel/trace"
)
type RepoEvent struct {
···
const TimeframeMonths = 7
-
func MakeProfileTimeline(e Execer, forDid string) (*ProfileTimeline, error) {
+
func MakeProfileTimeline(ctx context.Context, e Execer, forDid string) (*ProfileTimeline, error) {
+
span := trace.SpanFromContext(ctx)
+
defer span.End()
+
+
span.SetAttributes(
+
attribute.String("forDid", forDid),
+
)
+
timeline := ProfileTimeline{
ByMonth: make([]ByMonth, TimeframeMonths),
}
···
pulls, err := GetPullsByOwnerDid(e, forDid, timeframe)
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "error getting pulls by owner did")
return nil, fmt.Errorf("error getting pulls by owner did: %w", err)
}
+
span.SetAttributes(attribute.Int("pulls.count", len(pulls)))
+
// group pulls by month
for _, pull := range pulls {
pullMonth := pull.Created.Month()
···
issues, err := GetIssuesByOwnerDid(e, forDid, timeframe)
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "error getting issues by owner did")
return nil, fmt.Errorf("error getting issues by owner did: %w", err)
}
+
span.SetAttributes(attribute.Int("issues.count", len(issues)))
+
for _, issue := range issues {
issueMonth := issue.Created.Month()
···
*items = append(*items, &issue)
}
-
repos, err := GetAllReposByDid(e, forDid)
+
repos, err := GetAllReposByDid(ctx, e, forDid)
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "error getting all repos by did")
return nil, fmt.Errorf("error getting all repos by did: %w", err)
}
+
+
span.SetAttributes(attribute.Int("repos.count", len(repos)))
for _, repo := range repos {
// TODO: get this in the original query; requires COALESCE because nullable
var sourceRepo *Repo
if repo.Source != "" {
-
sourceRepo, err = GetRepoByAtUri(e, repo.Source)
+
sourceRepo, err = GetRepoByAtUri(ctx, e, repo.Source)
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "error getting repo by at uri")
return nil, err
}
}
+111 -25
appview/db/pulls.go
···
package db
import (
+
"context"
"database/sql"
"fmt"
"log"
···
"github.com/bluekeyes/go-gitdiff/gitdiff"
"github.com/bluesky-social/indigo/atproto/syntax"
+
"go.opentelemetry.io/otel/attribute"
+
"go.opentelemetry.io/otel/trace"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/patchutil"
"tangled.sh/tangled.sh/core/types"
···
return patches
}
-
func NewPull(tx *sql.Tx, pull *Pull) error {
+
func NewPull(ctx context.Context, tx *sql.Tx, pull *Pull) error {
+
span := trace.SpanFromContext(ctx)
+
defer span.End()
+
+
span.SetAttributes(
+
attribute.String("repo.at", pull.RepoAt.String()),
+
attribute.String("owner.did", pull.OwnerDid),
+
attribute.String("title", pull.Title),
+
attribute.String("target_branch", pull.TargetBranch),
+
)
+
span.AddEvent("creating new pull request")
+
defer tx.Rollback()
_, err := tx.Exec(`
···
values (?, 1)
`, pull.RepoAt)
if err != nil {
+
span.RecordError(err)
return err
}
···
returning next_pull_id - 1
`, pull.RepoAt).Scan(&nextId)
if err != nil {
+
span.RecordError(err)
return err
}
pull.PullId = nextId
pull.State = PullOpen
+
span.SetAttributes(attribute.Int("pull.id", pull.PullId))
+
span.AddEvent("assigned pull ID")
+
var sourceBranch, sourceRepoAt *string
if pull.PullSource != nil {
sourceBranch = &pull.PullSource.Branch
···
sourceRepoAt,
)
if err != nil {
+
span.RecordError(err)
return err
}
+
+
span.AddEvent("inserted pull record")
_, err = tx.Exec(`
insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev)
values (?, ?, ?, ?, ?)
`, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev)
if err != nil {
+
span.RecordError(err)
return err
}
+
+
span.AddEvent("inserted initial pull submission")
if err := tx.Commit(); err != nil {
+
span.RecordError(err)
return err
}
+
span.AddEvent("transaction committed successfully")
return nil
}
-
func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
-
pull, err := GetPull(e, repoAt, pullId)
+
func GetPullAt(ctx context.Context, e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
+
pull, err := GetPull(ctx, e, repoAt, pullId)
if err != nil {
return "", err
}
···
return pullId - 1, err
}
-
func GetPulls(e Execer, repoAt syntax.ATURI, state PullState) ([]*Pull, error) {
+
func GetPulls(ctx context.Context, e Execer, repoAt syntax.ATURI, state PullState) ([]*Pull, error) {
+
span := trace.SpanFromContext(ctx)
+
defer span.End()
+
+
span.SetAttributes(
+
attribute.String("repoAt", repoAt.String()),
+
attribute.String("state", state.String()),
+
)
+
span.AddEvent("querying pulls")
+
pulls := make(map[int]*Pull)
-
rows, err := e.Query(`
+
rows, err := e.QueryContext(ctx, `
select
owner_did,
pull_id,
···
where
repo_at = ? and state = ?`, repoAt, state)
if err != nil {
+
span.RecordError(err)
return nil, err
}
defer rows.Close()
···
&sourceRepoAt,
)
if err != nil {
+
span.RecordError(err)
return nil, err
}
createdTime, err := time.Parse(time.RFC3339, createdAt)
if err != nil {
+
span.RecordError(err)
return nil, err
}
pull.Created = createdTime
···
if sourceRepoAt.Valid {
sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String)
if err != nil {
+
span.RecordError(err)
return nil, err
}
pull.PullSource.RepoAt = &sourceRepoAtParsed
···
pulls[pull.PullId] = &pull
}
+
span.AddEvent("querying pull submissions")
+
span.SetAttributes(attribute.Int("pull_count", len(pulls)))
+
// get latest round no. for each pull
inClause := strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ")
submissionsQuery := fmt.Sprintf(`
···
args[idx] = p.PullId
idx += 1
}
-
submissionsRows, err := e.Query(submissionsQuery, args...)
+
submissionsRows, err := e.QueryContext(ctx, submissionsQuery, args...)
if err != nil {
+
span.RecordError(err)
return nil, err
}
defer submissionsRows.Close()
···
&s.RoundNumber,
)
if err != nil {
+
span.RecordError(err)
return nil, err
}
···
}
}
if err := rows.Err(); err != nil {
+
span.RecordError(err)
return nil, err
}
+
+
span.AddEvent("querying pull comments")
// get comment count on latest submission on each pull
inClause = strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ")
···
for _, p := range pulls {
args = append(args, p.Submissions[p.LastRoundNumber()].ID)
}
-
commentsRows, err := e.Query(commentsQuery, args...)
+
commentsRows, err := e.QueryContext(ctx, commentsQuery, args...)
if err != nil {
+
span.RecordError(err)
return nil, err
}
defer commentsRows.Close()
···
&pullId,
)
if err != nil {
+
span.RecordError(err)
return nil, err
}
if p, ok := pulls[pullId]; ok {
···
}
}
if err := rows.Err(); err != nil {
+
span.RecordError(err)
return nil, err
}
+
+
span.AddEvent("sorting pulls by date")
orderedByDate := []*Pull{}
for _, p := range pulls {
···
return orderedByDate[i].Created.After(orderedByDate[j].Created)
})
+
span.SetAttributes(attribute.Int("result_count", len(orderedByDate)))
return orderedByDate, nil
}
-
func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) {
+
func GetPull(ctx context.Context, e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) {
+
span := trace.SpanFromContext(ctx)
+
defer span.End()
+
+
span.SetAttributes(attribute.String("repoAt", repoAt.String()), attribute.Int("pull.id", pullId))
+
span.AddEvent("query pull metadata")
+
query := `
select
owner_did,
···
where
repo_at = ? and pull_id = ?
`
-
row := e.QueryRow(query, repoAt, pullId)
+
row := e.QueryRowContext(ctx, query, repoAt, pullId)
var pull Pull
var createdAt string
···
&sourceRepoAt,
)
if err != nil {
+
span.RecordError(err)
return nil, err
}
createdTime, err := time.Parse(time.RFC3339, createdAt)
if err != nil {
+
span.RecordError(err)
return nil, err
}
pull.Created = createdTime
-
// populate source
if sourceBranch.Valid {
pull.PullSource = &PullSource{
Branch: sourceBranch.String,
···
if sourceRepoAt.Valid {
sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String)
if err != nil {
+
span.RecordError(err)
return nil, err
}
pull.PullSource.RepoAt = &sourceRepoAtParsed
}
}
+
span.AddEvent("query submissions")
submissionsQuery := `
select
id, pull_id, repo_at, round_number, patch, created, source_rev
···
where
repo_at = ? and pull_id = ?
`
-
submissionsRows, err := e.Query(submissionsQuery, repoAt, pullId)
+
submissionsRows, err := e.QueryContext(ctx, submissionsQuery, repoAt, pullId)
if err != nil {
+
span.RecordError(err)
return nil, err
}
defer submissionsRows.Close()
···
&submissionSourceRev,
)
if err != nil {
+
span.RecordError(err)
return nil, err
}
submissionCreatedTime, err := time.Parse(time.RFC3339, submissionCreatedStr)
if err != nil {
+
span.RecordError(err)
return nil, err
}
submission.Created = submissionCreatedTime
···
submissionsMap[submission.ID] = &submission
}
if err = submissionsRows.Close(); err != nil {
+
span.RecordError(err)
return nil, err
}
if len(submissionsMap) == 0 {
···
args = append(args, k)
}
inClause := strings.TrimSuffix(strings.Repeat("?, ", len(submissionsMap)), ", ")
+
+
span.AddEvent("query comments")
commentsQuery := fmt.Sprintf(`
select
id,
···
order by
created asc
`, inClause)
-
commentsRows, err := e.Query(commentsQuery, args...)
+
commentsRows, err := e.QueryContext(ctx, commentsQuery, args...)
if err != nil {
+
span.RecordError(err)
return nil, err
}
defer commentsRows.Close()
···
&commentCreatedStr,
)
if err != nil {
+
span.RecordError(err)
return nil, err
}
commentCreatedTime, err := time.Parse(time.RFC3339, commentCreatedStr)
if err != nil {
+
span.RecordError(err)
return nil, err
}
comment.Created = commentCreatedTime
-
// Add the comment to its submission
if submission, ok := submissionsMap[comment.SubmissionId]; ok {
submission.Comments = append(submission.Comments, comment)
}
-
}
if err = commentsRows.Err(); err != nil {
+
span.RecordError(err)
return nil, err
}
-
var pullSourceRepo *Repo
-
if pull.PullSource != nil {
-
if pull.PullSource.RepoAt != nil {
-
pullSourceRepo, err = GetRepoByAtUri(e, pull.PullSource.RepoAt.String())
-
if err != nil {
-
log.Printf("failed to get repo by at uri: %v", err)
-
} else {
-
pull.PullSource.Repo = pullSourceRepo
-
}
+
if pull.PullSource != nil && pull.PullSource.RepoAt != nil {
+
span.AddEvent("query pull source repo")
+
pullSourceRepo, err := GetRepoByAtUri(ctx, e, pull.PullSource.RepoAt.String())
+
if err != nil {
+
span.RecordError(err)
+
log.Printf("failed to get repo by at uri: %v", err)
+
} else {
+
pull.PullSource.Repo = pullSourceRepo
}
}
···
return pulls, nil
}
-
func NewPullComment(e Execer, comment *PullComment) (int64, error) {
+
func NewPullComment(ctx context.Context, e Execer, comment *PullComment) (int64, error) {
+
span := trace.SpanFromContext(ctx)
+
defer span.End()
+
+
span.SetAttributes(
+
attribute.String("repo.at", comment.RepoAt),
+
attribute.Int("pull.id", comment.PullId),
+
attribute.Int("submission.id", comment.SubmissionId),
+
attribute.String("owner.did", comment.OwnerDid),
+
)
+
span.AddEvent("inserting new pull comment")
+
query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)`
-
res, err := e.Exec(
+
res, err := e.ExecContext(
+
ctx,
query,
comment.OwnerDid,
comment.RepoAt,
···
comment.Body,
)
if err != nil {
+
span.RecordError(err)
return 0, err
}
i, err := res.LastInsertId()
if err != nil {
+
span.RecordError(err)
return 0, err
}
+
span.SetAttributes(attribute.Int64("comment.id", i))
+
span.AddEvent("pull comment created successfully")
return i, nil
}
+114 -12
appview/db/repos.go
···
package db
import (
+
"context"
"database/sql"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
+
"go.opentelemetry.io/otel"
+
"go.opentelemetry.io/otel/attribute"
)
type Repo struct {
···
Source string
}
-
func GetAllRepos(e Execer, limit int) ([]Repo, error) {
+
func GetAllRepos(ctx context.Context, e Execer, limit int) ([]Repo, error) {
+
ctx, span := otel.Tracer("db").Start(ctx, "GetAllRepos")
+
defer span.End()
+
span.SetAttributes(attribute.Int("limit", limit))
+
var repos []Repo
rows, err := e.Query(
···
limit,
)
if err != nil {
+
span.RecordError(err)
return nil, err
}
defer rows.Close()
···
rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &repo.Description, &repo.Created, &repo.Source,
)
if err != nil {
+
span.RecordError(err)
return nil, err
}
repos = append(repos, repo)
}
if err := rows.Err(); err != nil {
+
span.RecordError(err)
return nil, err
}
+
span.SetAttributes(attribute.Int("repos.count", len(repos)))
return repos, nil
}
-
func GetAllReposByDid(e Execer, did string) ([]Repo, error) {
+
func GetAllReposByDid(ctx context.Context, e Execer, did string) ([]Repo, error) {
+
ctx, span := otel.Tracer("db").Start(ctx, "GetAllReposByDid")
+
defer span.End()
+
span.SetAttributes(attribute.String("did", did))
+
var repos []Repo
rows, err := e.Query(
···
order by r.created desc`,
did)
if err != nil {
+
span.RecordError(err)
return nil, err
}
defer rows.Close()
···
err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount, &nullableSource)
if err != nil {
+
span.RecordError(err)
return nil, err
}
···
}
if err := rows.Err(); err != nil {
+
span.RecordError(err)
return nil, err
}
+
span.SetAttributes(attribute.Int("repos.count", len(repos)))
return repos, nil
}
-
func GetRepo(e Execer, did, name string) (*Repo, error) {
+
func GetRepo(ctx context.Context, e Execer, did, name string) (*Repo, error) {
+
ctx, span := otel.Tracer("db").Start(ctx, "GetRepo")
+
defer span.End()
+
span.SetAttributes(
+
attribute.String("did", did),
+
attribute.String("name", name),
+
)
+
var repo Repo
var nullableDescription sql.NullString
···
var createdAt string
if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil {
+
span.RecordError(err)
return nil, err
}
createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
···
return &repo, nil
}
-
func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) {
+
func GetRepoByAtUri(ctx context.Context, e Execer, atUri string) (*Repo, error) {
+
ctx, span := otel.Tracer("db").Start(ctx, "GetRepoByAtUri")
+
defer span.End()
+
span.SetAttributes(attribute.String("atUri", atUri))
+
var repo Repo
var nullableDescription sql.NullString
···
var createdAt string
if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil {
+
span.RecordError(err)
return nil, err
}
createdAtTime, _ := time.Parse(time.RFC3339, createdAt)
···
return &repo, nil
}
-
func AddRepo(e Execer, repo *Repo) error {
+
func AddRepo(ctx context.Context, e Execer, repo *Repo) error {
+
ctx, span := otel.Tracer("db").Start(ctx, "AddRepo")
+
defer span.End()
+
span.SetAttributes(
+
attribute.String("did", repo.Did),
+
attribute.String("name", repo.Name),
+
)
+
_, err := e.Exec(
`insert into repos
(did, name, knot, rkey, at_uri, description, source)
values (?, ?, ?, ?, ?, ?, ?)`,
repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source,
)
+
if err != nil {
+
span.RecordError(err)
+
}
return err
}
-
func RemoveRepo(e Execer, did, name string) error {
+
func RemoveRepo(ctx context.Context, e Execer, did, name string) error {
+
ctx, span := otel.Tracer("db").Start(ctx, "RemoveRepo")
+
defer span.End()
+
span.SetAttributes(
+
attribute.String("did", did),
+
attribute.String("name", name),
+
)
+
_, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name)
+
if err != nil {
+
span.RecordError(err)
+
}
return err
}
-
func GetRepoSource(e Execer, repoAt syntax.ATURI) (string, error) {
+
func GetRepoSource(ctx context.Context, e Execer, repoAt syntax.ATURI) (string, error) {
+
ctx, span := otel.Tracer("db").Start(ctx, "GetRepoSource")
+
defer span.End()
+
span.SetAttributes(attribute.String("repoAt", repoAt.String()))
+
var nullableSource sql.NullString
err := e.QueryRow(`select source from repos where at_uri = ?`, repoAt).Scan(&nullableSource)
if err != nil {
+
span.RecordError(err)
return "", err
}
return nullableSource.String, nil
}
-
func GetForksByDid(e Execer, did string) ([]Repo, error) {
+
func GetForksByDid(ctx context.Context, e Execer, did string) ([]Repo, error) {
+
ctx, span := otel.Tracer("db").Start(ctx, "GetForksByDid")
+
defer span.End()
+
span.SetAttributes(attribute.String("did", did))
+
var repos []Repo
rows, err := e.Query(
···
did,
)
if err != nil {
+
span.RecordError(err)
return nil, err
}
defer rows.Close()
···
err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource)
if err != nil {
+
span.RecordError(err)
return nil, err
}
···
}
if err := rows.Err(); err != nil {
+
span.RecordError(err)
return nil, err
}
+
span.SetAttributes(attribute.Int("forks.count", len(repos)))
return repos, nil
}
-
func GetForkByDid(e Execer, did string, name string) (*Repo, error) {
+
func GetForkByDid(ctx context.Context, e Execer, did string, name string) (*Repo, error) {
+
ctx, span := otel.Tracer("db").Start(ctx, "GetForkByDid")
+
defer span.End()
+
span.SetAttributes(
+
attribute.String("did", did),
+
attribute.String("name", name),
+
)
+
var repo Repo
var createdAt string
var nullableDescription sql.NullString
···
err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource)
if err != nil {
+
span.RecordError(err)
return nil, err
}
···
return &repo, nil
}
-
func AddCollaborator(e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error {
+
func AddCollaborator(ctx context.Context, e Execer, collaborator, repoOwnerDid, repoName, repoKnot string) error {
+
ctx, span := otel.Tracer("db").Start(ctx, "AddCollaborator")
+
defer span.End()
+
span.SetAttributes(
+
attribute.String("collaborator", collaborator),
+
attribute.String("repoOwnerDid", repoOwnerDid),
+
attribute.String("repoName", repoName),
+
)
+
_, err := e.Exec(
`insert into collaborators (did, repo)
values (?, (select id from repos where did = ? and name = ? and knot = ?));`,
collaborator, repoOwnerDid, repoName, repoKnot)
+
if err != nil {
+
span.RecordError(err)
+
}
return err
}
-
func UpdateDescription(e Execer, repoAt, newDescription string) error {
+
func UpdateDescription(ctx context.Context, e Execer, repoAt, newDescription string) error {
+
ctx, span := otel.Tracer("db").Start(ctx, "UpdateDescription")
+
defer span.End()
+
span.SetAttributes(
+
attribute.String("repoAt", repoAt),
+
attribute.String("description", newDescription),
+
)
+
_, err := e.Exec(
`update repos set description = ? where at_uri = ?`, newDescription, repoAt)
+
if err != nil {
+
span.RecordError(err)
+
}
return err
}
-
func CollaboratingIn(e Execer, collaborator string) ([]Repo, error) {
+
func CollaboratingIn(ctx context.Context, e Execer, collaborator string) ([]Repo, error) {
+
ctx, span := otel.Tracer("db").Start(ctx, "CollaboratingIn")
+
defer span.End()
+
span.SetAttributes(attribute.String("collaborator", collaborator))
+
var repos []Repo
rows, err := e.Query(
···
group by
r.id;`, collaborator)
if err != nil {
+
span.RecordError(err)
return nil, err
}
defer rows.Close()
···
err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repoStats.StarCount)
if err != nil {
+
span.RecordError(err)
return nil, err
}
···
}
if err := rows.Err(); err != nil {
+
span.RecordError(err)
return nil, err
}
+
span.SetAttributes(attribute.Int("repos.count", len(repos)))
return repos, nil
}
+5 -4
appview/db/star.go
···
package db
import (
+
"context"
"log"
"time"
···
Repo *Repo
}
-
func (star *Star) ResolveRepo(e Execer) error {
+
func (star *Star) ResolveRepo(ctx context.Context, e Execer) error {
if star.Repo != nil {
return nil
}
-
repo, err := GetRepoByAtUri(e, star.RepoAt.String())
+
repo, err := GetRepoByAtUri(ctx, e, star.RepoAt.String())
if err != nil {
return err
}
···
// Get a star record
func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*Star, error) {
query := `
-
select starred_by_did, repo_at, created, rkey
+
select starred_by_did, repo_at, created, rkey
from stars
where starred_by_did = ? and repo_at = ?`
row := e.QueryRow(query, starredByDid, repoAt)
···
var stars []Star
rows, err := e.Query(`
-
select
+
select
s.starred_by_did,
s.repo_at,
s.rkey,
+28 -3
appview/db/timeline.go
···
package db
import (
+
"context"
"sort"
"time"
+
+
"go.opentelemetry.io/otel/attribute"
+
"go.opentelemetry.io/otel/trace"
)
type TimelineEvent struct {
···
// TODO: this gathers heterogenous events from different sources and aggregates
// them in code; if we did this entirely in sql, we could order and limit and paginate easily
-
func MakeTimeline(e Execer) ([]TimelineEvent, error) {
+
func MakeTimeline(ctx context.Context, e Execer) ([]TimelineEvent, error) {
+
span := trace.SpanFromContext(ctx)
+
defer span.End()
+
var events []TimelineEvent
limit := 50
-
repos, err := GetAllRepos(e, limit)
+
span.SetAttributes(attribute.Int("timeline.limit", limit))
+
+
repos, err := GetAllRepos(ctx, e, limit)
if err != nil {
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error.from", "GetAllRepos"))
return nil, err
}
+
span.SetAttributes(attribute.Int("timeline.repos.count", len(repos)))
follows, err := GetAllFollows(e, limit)
if err != nil {
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error.from", "GetAllFollows"))
return nil, err
}
+
span.SetAttributes(attribute.Int("timeline.follows.count", len(follows)))
stars, err := GetAllStars(e, limit)
if err != nil {
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error.from", "GetAllStars"))
return nil, err
}
+
span.SetAttributes(attribute.Int("timeline.stars.count", len(stars)))
for _, repo := range repos {
var sourceRepo *Repo
if repo.Source != "" {
-
sourceRepo, err = GetRepoByAtUri(e, repo.Source)
+
sourceRepo, err = GetRepoByAtUri(ctx, e, repo.Source)
if err != nil {
+
span.RecordError(err)
+
span.SetAttributes(
+
attribute.String("error.from", "GetRepoByAtUri"),
+
attribute.String("repo.source", repo.Source),
+
)
return nil, err
}
}
···
if len(events) > limit {
events = events[:limit]
}
+
+
span.SetAttributes(attribute.Int("timeline.events.total", len(events)))
return events, nil
}
+1 -1
appview/state/artifact.go
···
s.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(r.Context(), s, user),
Artifact: artifact,
})
}
+31 -13
appview/state/middleware.go
···
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/go-chi/chi/v5"
+
"go.opentelemetry.io/otel/attribute"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/middleware"
)
···
func knotRoleMiddleware(s *State, group string) middleware.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "knotRoleMiddleware")
+
defer span.End()
+
// requires auth also
-
actor := s.auth.GetUser(r)
+
actor := s.auth.GetUser(r.WithContext(ctx))
if actor == nil {
// we need a logged in user
log.Printf("not logged in, redirecting")
···
return
}
-
next.ServeHTTP(w, r)
+
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
···
func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "RepoPermissionMiddleware")
+
defer span.End()
+
// requires auth also
-
actor := s.auth.GetUser(r)
+
actor := s.auth.GetUser(r.WithContext(ctx))
if actor == nil {
// we need a logged in user
log.Printf("not logged in, redirecting")
http.Error(w, "Forbiden", http.StatusUnauthorized)
return
}
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
http.Error(w, "malformed url", http.StatusBadRequest)
return
···
return
}
-
next.ServeHTTP(w, r)
+
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
···
return
}
-
id, err := s.resolver.ResolveIdent(req.Context(), didOrHandle)
+
ctx, span := s.t.TraceStart(req.Context(), "ResolveIdent")
+
defer span.End()
+
+
id, err := s.resolver.ResolveIdent(ctx, didOrHandle)
if err != nil {
// invalid did or handle
log.Println("failed to resolve did/handle:", err)
···
return
}
-
ctx := context.WithValue(req.Context(), "resolvedId", *id)
+
ctx = context.WithValue(ctx, "resolvedId", *id)
next.ServeHTTP(w, req.WithContext(ctx))
})
···
func ResolveRepo(s *State) middleware.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+
ctx, span := s.t.TraceStart(req.Context(), "ResolveRepo")
+
defer span.End()
+
repoName := chi.URLParam(req, "repo")
-
id, ok := req.Context().Value("resolvedId").(identity.Identity)
+
id, ok := ctx.Value("resolvedId").(identity.Identity)
if !ok {
log.Println("malformed middleware")
w.WriteHeader(http.StatusInternalServerError)
return
}
-
repo, err := db.GetRepo(s.db, id.DID.String(), repoName)
+
repo, err := db.GetRepo(ctx, s.db, id.DID.String(), repoName)
if err != nil {
// invalid did or handle
log.Println("failed to resolve repo")
···
return
}
-
ctx := context.WithValue(req.Context(), "knot", repo.Knot)
+
ctx = context.WithValue(ctx, "knot", repo.Knot)
ctx = context.WithValue(ctx, "repoAt", repo.AtUri)
ctx = context.WithValue(ctx, "repoDescription", repo.Description)
ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339))
···
func ResolvePull(s *State) middleware.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "ResolvePull")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to fully resolve repo", err)
http.Error(w, "invalid repo url", http.StatusNotFound)
···
return
}
-
pr, err := db.GetPull(s.db, f.RepoAt, prIdInt)
+
pr, err := db.GetPull(ctx, s.db, f.RepoAt, prIdInt)
if err != nil {
log.Println("failed to get pull and comments", err)
return
}
-
ctx := context.WithValue(r.Context(), "pull", pr)
+
span.SetAttributes(attribute.Int("pull.id", prIdInt))
+
+
ctx = context.WithValue(ctx, "pull", pr)
next.ServeHTTP(w, r.WithContext(ctx))
})
+33 -5
appview/state/profile.go
···
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/go-chi/chi/v5"
+
"go.opentelemetry.io/otel/attribute"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages"
)
func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "ProfilePage")
+
defer span.End()
+
didOrHandle := chi.URLParam(r, "user")
if didOrHandle == "" {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
-
ident, ok := r.Context().Value("resolvedId").(identity.Identity)
+
ident, ok := ctx.Value("resolvedId").(identity.Identity)
if !ok {
s.pages.Error404(w)
+
span.RecordError(fmt.Errorf("failed to resolve identity"))
return
}
-
repos, err := db.GetAllReposByDid(s.db, ident.DID.String())
+
span.SetAttributes(
+
attribute.String("user.did", ident.DID.String()),
+
attribute.String("user.handle", ident.Handle.String()),
+
)
+
+
repos, err := db.GetAllReposByDid(ctx, s.db, ident.DID.String())
if err != nil {
log.Printf("getting repos for %s: %s", ident.DID.String(), err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error.repos", err.Error()))
}
+
span.SetAttributes(attribute.Int("repos.count", len(repos)))
-
collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String())
+
collaboratingRepos, err := db.CollaboratingIn(ctx, s.db, ident.DID.String())
if err != nil {
log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error.collaborating_repos", err.Error()))
}
+
span.SetAttributes(attribute.Int("collaborating_repos.count", len(collaboratingRepos)))
-
timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String())
+
timeline, err := db.MakeProfileTimeline(ctx, s.db, ident.DID.String())
if err != nil {
log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error.timeline", err.Error()))
}
var didsToResolve []string
···
}
}
}
+
span.SetAttributes(attribute.Int("dids_to_resolve.count", len(didsToResolve)))
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
+
resolvedIds := s.resolver.ResolveIdents(ctx, didsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
+
span.SetAttributes(attribute.Int("resolved_ids.count", len(resolvedIds)))
followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String())
if err != nil {
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error.follow_stats", err.Error()))
}
+
span.SetAttributes(
+
attribute.Int("followers.count", followers),
+
attribute.Int("following.count", following),
+
)
loggedInUser := s.auth.GetUser(r)
followStatus := db.IsNotFollowing
if loggedInUser != nil {
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
+
span.SetAttributes(attribute.String("logged_in_user.did", loggedInUser.Did))
}
+
span.SetAttributes(attribute.String("follow_status", string(db.FollowStatus(followStatus))))
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
s.pages.ProfilePage(w, pages.ProfilePageParams{
+633 -125
appview/state/pull.go
···
package state
import (
+
"context"
"database/sql"
"encoding/json"
"errors"
···
"strconv"
"time"
+
"go.opentelemetry.io/otel/attribute"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/auth"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/patchutil"
+
"tangled.sh/tangled.sh/core/telemetry"
"tangled.sh/tangled.sh/core/types"
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
// htmx fragment
func (s *State) PullActions(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "PullActions")
+
defer span.End()
+
switch r.Method {
case http.MethodGet:
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
pull, ok := r.Context().Value("pull").(*db.Pull)
+
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
···
return
}
-
mergeCheckResponse := s.mergeCheck(f, pull)
+
_, mergeSpan := s.t.TraceStart(ctx, "mergeCheck")
+
mergeCheckResponse := s.mergeCheck(ctx, f, pull)
+
mergeSpan.End()
+
resubmitResult := pages.Unknown
if user.Did == pull.OwnerDid {
-
resubmitResult = s.resubmitCheck(f, pull)
+
_, resubmitSpan := s.t.TraceStart(ctx, "resubmitCheck")
+
resubmitResult = s.resubmitCheck(ctx, f, pull)
+
resubmitSpan.End()
}
+
_, renderSpan := s.t.TraceStart(ctx, "renderPullActions")
s.pages.PullActionsFragment(w, pages.PullActionsParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
Pull: pull,
RoundNumber: roundNumber,
MergeCheck: mergeCheckResponse,
ResubmitCheck: resubmitResult,
})
+
renderSpan.End()
return
}
}
func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "RepoSinglePull")
+
defer span.End()
+
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
return
}
-
pull, ok := r.Context().Value("pull").(*db.Pull)
+
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
-
log.Println("failed to get pull")
+
err := errors.New("failed to get pull from context")
+
log.Println(err)
+
span.RecordError(err)
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
+
attrs := telemetry.MapAttrs[string](map[string]string{
+
"pull.id": fmt.Sprintf("%d", pull.PullId),
+
"pull.owner": pull.OwnerDid,
+
})
+
+
span.SetAttributes(attrs...)
+
totalIdents := 1
for _, submission := range pull.Submissions {
totalIdents += len(submission.Comments)
···
}
}
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
+
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
+
span.SetAttributes(attribute.Int("identities.resolved", len(resolvedIds)))
-
mergeCheckResponse := s.mergeCheck(f, pull)
+
mergeCheckResponse := s.mergeCheck(ctx, f, pull)
+
resubmitResult := pages.Unknown
if user != nil && user.Did == pull.OwnerDid {
-
resubmitResult = s.resubmitCheck(f, pull)
+
resubmitResult = s.resubmitCheck(ctx, f, pull)
}
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
DidHandleMap: didHandleMap,
Pull: pull,
MergeCheck: mergeCheckResponse,
···
})
}
-
func (s *State) mergeCheck(f *FullyResolvedRepo, pull *db.Pull) types.MergeCheckResponse {
+
func (s *State) mergeCheck(ctx context.Context, f *FullyResolvedRepo, pull *db.Pull) types.MergeCheckResponse {
if pull.State == db.PullMerged {
return types.MergeCheckResponse{}
}
···
return mergeCheckResponse
}
-
func (s *State) resubmitCheck(f *FullyResolvedRepo, pull *db.Pull) pages.ResubmitResult {
+
func (s *State) resubmitCheck(ctx context.Context, f *FullyResolvedRepo, pull *db.Pull) pages.ResubmitResult {
+
ctx, span := s.t.TraceStart(ctx, "resubmitCheck")
+
defer span.End()
+
+
span.SetAttributes(attribute.Int("pull.id", pull.PullId))
+
if pull.State == db.PullMerged || pull.PullSource == nil {
+
span.SetAttributes(attribute.String("result", "Unknown"))
return pages.Unknown
}
···
if pull.PullSource.RepoAt != nil {
// fork-based pulls
-
sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
+
span.SetAttributes(attribute.Bool("isForkBased", true))
+
sourceRepo, err := db.GetRepoByAtUri(ctx, s.db, pull.PullSource.RepoAt.String())
if err != nil {
log.Println("failed to get source repo", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "failed_to_get_source_repo"))
+
span.SetAttributes(attribute.String("result", "Unknown"))
return pages.Unknown
}
···
repoName = sourceRepo.Name
} else {
// pulls within the same repo
+
span.SetAttributes(attribute.Bool("isBranchBased", true))
knot = f.Knot
ownerDid = f.OwnerDid()
repoName = f.RepoName
}
+
span.SetAttributes(
+
attribute.String("knot", knot),
+
attribute.String("ownerDid", ownerDid),
+
attribute.String("repoName", repoName),
+
attribute.String("sourceBranch", pull.PullSource.Branch),
+
)
+
us, err := NewUnsignedClient(knot, s.config.Dev)
if err != nil {
log.Printf("failed to setup client for %s; ignoring: %v", knot, err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "failed_to_setup_client"))
+
span.SetAttributes(attribute.String("result", "Unknown"))
return pages.Unknown
}
resp, err := us.Branch(ownerDid, repoName, pull.PullSource.Branch)
if err != nil {
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "failed_to_reach_knotserver"))
+
span.SetAttributes(attribute.String("result", "Unknown"))
return pages.Unknown
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("error reading response body: %v", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "failed_to_read_response"))
+
span.SetAttributes(attribute.String("result", "Unknown"))
return pages.Unknown
}
defer resp.Body.Close()
···
var result types.RepoBranchResponse
if err := json.Unmarshal(body, &result); err != nil {
log.Println("failed to parse response:", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "failed_to_parse_response"))
+
span.SetAttributes(attribute.String("result", "Unknown"))
return pages.Unknown
}
latestSubmission := pull.Submissions[pull.LastRoundNumber()]
+
+
span.SetAttributes(
+
attribute.String("latestSubmission.SourceRev", latestSubmission.SourceRev),
+
attribute.String("branch.Hash", result.Branch.Hash),
+
)
+
if latestSubmission.SourceRev != result.Branch.Hash {
fmt.Println(latestSubmission.SourceRev, result.Branch.Hash)
+
span.SetAttributes(attribute.String("result", "ShouldResubmit"))
return pages.ShouldResubmit
}
+
span.SetAttributes(attribute.String("result", "ShouldNotResubmit"))
return pages.ShouldNotResubmit
}
func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "RepoPullPatch")
+
defer span.End()
+
+
user := s.auth.GetUser(r.WithContext(ctx))
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
return
}
-
pull, ok := r.Context().Value("pull").(*db.Pull)
+
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
-
log.Println("failed to get pull")
+
err := errors.New("failed to get pull from context")
+
log.Println(err)
+
span.RecordError(err)
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
···
if err != nil || roundIdInt >= len(pull.Submissions) {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("failed to parse round id", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "bad_round_id"))
return
}
+
span.SetAttributes(
+
attribute.Int("pull.id", pull.PullId),
+
attribute.Int("round", roundIdInt),
+
attribute.String("pull.owner", pull.OwnerDid),
+
)
+
identsToResolve := []string{pull.OwnerDid}
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
+
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
+
span.SetAttributes(attribute.Int("identities.resolved", len(resolvedIds)))
diff := pull.Submissions[roundIdInt].AsNiceDiff(pull.TargetBranch)
s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{
LoggedInUser: user,
DidHandleMap: didHandleMap,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
Pull: pull,
Round: roundIdInt,
Submission: pull.Submissions[roundIdInt],
Diff: &diff,
})
-
}
func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "RepoPullInterdiff")
+
defer span.End()
+
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
pull, ok := r.Context().Value("pull").(*db.Pull)
+
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to get pull.")
return
}
+
_, roundSpan := s.t.TraceStart(ctx, "parseRound")
roundId := chi.URLParam(r, "round")
roundIdInt, err := strconv.Atoi(roundId)
if err != nil || roundIdInt >= len(pull.Submissions) {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("failed to parse round id", err)
+
roundSpan.End()
return
}
if roundIdInt == 0 {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("cannot interdiff initial submission")
+
roundSpan.End()
return
}
+
roundSpan.End()
+
_, identSpan := s.t.TraceStart(ctx, "resolveIdentities")
identsToResolve := []string{pull.OwnerDid}
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
+
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
+
identSpan.End()
+
_, diffSpan := s.t.TraceStart(ctx, "calculateInterdiff")
currentPatch, err := pull.Submissions[roundIdInt].AsDiff(pull.TargetBranch)
if err != nil {
log.Println("failed to interdiff; current patch malformed")
s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.")
+
diffSpan.End()
return
}
···
if err != nil {
log.Println("failed to interdiff; previous patch malformed")
s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.")
+
diffSpan.End()
return
}
interdiff := patchutil.Interdiff(previousPatch, currentPatch)
+
diffSpan.End()
+
_, renderSpan := s.t.TraceStart(ctx, "renderInterdiffPage")
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
-
LoggedInUser: s.auth.GetUser(r),
-
RepoInfo: f.RepoInfo(s, user),
+
LoggedInUser: s.auth.GetUser(r.WithContext(ctx)),
+
RepoInfo: f.RepoInfo(ctx, s, user),
Pull: pull,
Round: roundIdInt,
DidHandleMap: didHandleMap,
Interdiff: interdiff,
})
+
renderSpan.End()
return
}
func (s *State) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) {
-
pull, ok := r.Context().Value("pull").(*db.Pull)
+
ctx, span := s.t.TraceStart(r.Context(), "RepoPullPatchRaw")
+
defer span.End()
+
+
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
+
_, roundSpan := s.t.TraceStart(ctx, "parseRound")
roundId := chi.URLParam(r, "round")
roundIdInt, err := strconv.Atoi(roundId)
if err != nil || roundIdInt >= len(pull.Submissions) {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("failed to parse round id", err)
+
roundSpan.End()
return
}
+
roundSpan.End()
+
_, identSpan := s.t.TraceStart(ctx, "resolveIdentities")
identsToResolve := []string{pull.OwnerDid}
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
+
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
+
identSpan.End()
+
_, writeSpan := s.t.TraceStart(ctx, "writePatch")
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(pull.Submissions[roundIdInt].Patch))
+
writeSpan.End()
}
func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "RepoPulls")
+
defer span.End()
+
user := s.auth.GetUser(r)
params := r.URL.Query()
+
_, stateSpan := s.t.TraceStart(ctx, "determinePullState")
state := db.PullOpen
switch params.Get("state") {
case "closed":
···
case "merged":
state = db.PullMerged
}
+
stateSpan.End()
-
f, err := s.fullyResolvedRepo(r)
+
_, repoSpan := s.t.TraceStart(ctx, "resolveRepo")
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
repoSpan.End()
return
}
+
repoSpan.End()
-
pulls, err := db.GetPulls(s.db, f.RepoAt, state)
+
_, pullsSpan := s.t.TraceStart(ctx, "getPulls")
+
pulls, err := db.GetPulls(ctx, s.db, f.RepoAt, state)
if err != nil {
log.Println("failed to get pulls", err)
s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.")
+
pullsSpan.End()
return
}
+
pullsSpan.End()
+
_, sourceRepoSpan := s.t.TraceStart(ctx, "resolvePullSources")
for _, p := range pulls {
var pullSourceRepo *db.Repo
if p.PullSource != nil {
if p.PullSource.RepoAt != nil {
-
pullSourceRepo, err = db.GetRepoByAtUri(s.db, p.PullSource.RepoAt.String())
+
pullSourceRepo, err = db.GetRepoByAtUri(ctx, s.db, p.PullSource.RepoAt.String())
if err != nil {
log.Printf("failed to get repo by at uri: %v", err)
continue
···
}
}
}
+
sourceRepoSpan.End()
+
_, identSpan := s.t.TraceStart(ctx, "resolveIdentities")
identsToResolve := make([]string, len(pulls))
for i, pull := range pulls {
identsToResolve[i] = pull.OwnerDid
}
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
+
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
+
identSpan.End()
+
_, renderSpan := s.t.TraceStart(ctx, "renderPullsPage")
s.pages.RepoPulls(w, pages.RepoPullsParams{
-
LoggedInUser: s.auth.GetUser(r),
-
RepoInfo: f.RepoInfo(s, user),
+
LoggedInUser: s.auth.GetUser(r.WithContext(ctx)),
+
RepoInfo: f.RepoInfo(ctx, s, user),
Pulls: pulls,
DidHandleMap: didHandleMap,
FilteringBy: state,
})
+
renderSpan.End()
return
}
func (s *State) PullComment(w http.ResponseWriter, r *http.Request) {
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "PullComment")
+
defer span.End()
+
+
user := s.auth.GetUser(r.WithContext(ctx))
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
pull, ok := r.Context().Value("pull").(*db.Pull)
+
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
+
_, roundSpan := s.t.TraceStart(ctx, "parseRoundNumber")
roundNumberStr := chi.URLParam(r, "round")
roundNumber, err := strconv.Atoi(roundNumberStr)
if err != nil || roundNumber >= len(pull.Submissions) {
http.Error(w, "bad round id", http.StatusBadRequest)
log.Println("failed to parse round id", err)
+
roundSpan.End()
return
}
+
roundSpan.End()
switch r.Method {
case http.MethodGet:
+
_, renderSpan := s.t.TraceStart(ctx, "renderCommentFragment")
s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
Pull: pull,
RoundNumber: roundNumber,
})
+
renderSpan.End()
return
case http.MethodPost:
+
postCtx, postSpan := s.t.TraceStart(ctx, "CreateComment")
+
defer postSpan.End()
+
+
_, validateSpan := s.t.TraceStart(postCtx, "validateComment")
body := r.FormValue("body")
if body == "" {
s.pages.Notice(w, "pull", "Comment body is required")
+
validateSpan.End()
return
}
+
validateSpan.End()
// Start a transaction
-
tx, err := s.db.BeginTx(r.Context(), nil)
+
_, txSpan := s.t.TraceStart(postCtx, "startTransaction")
+
tx, err := s.db.BeginTx(postCtx, nil)
if err != nil {
log.Println("failed to start transaction", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
+
txSpan.End()
return
}
defer tx.Rollback()
+
txSpan.End()
createdAt := time.Now().Format(time.RFC3339)
ownerDid := user.Did
-
pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId)
+
_, pullAtSpan := s.t.TraceStart(postCtx, "getPullAt")
+
pullAt, err := db.GetPullAt(postCtx, s.db, f.RepoAt, pull.PullId)
if err != nil {
log.Println("failed to get pull at", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
+
pullAtSpan.End()
return
}
+
pullAtSpan.End()
+
_, atProtoSpan := s.t.TraceStart(postCtx, "createAtProtoRecord")
atUri := f.RepoAt.String()
-
client, _ := s.auth.AuthorizedClient(r)
-
atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
client, _ := s.auth.AuthorizedClient(r.WithContext(postCtx))
+
atResp, err := comatproto.RepoPutRecord(postCtx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullCommentNSID,
Repo: user.Did,
Rkey: appview.TID(),
···
if err != nil {
log.Println("failed to create pull comment", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
+
atProtoSpan.End()
return
}
+
atProtoSpan.End()
// Create the pull comment in the database with the commentAt field
-
commentId, err := db.NewPullComment(tx, &db.PullComment{
+
_, dbSpan := s.t.TraceStart(postCtx, "createDbComment")
+
commentId, err := db.NewPullComment(postCtx, tx, &db.PullComment{
OwnerDid: user.Did,
RepoAt: f.RepoAt.String(),
PullId: pull.PullId,
···
if err != nil {
log.Println("failed to create pull comment", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
+
dbSpan.End()
return
}
+
dbSpan.End()
-
// Commit the transaction
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
s.pages.Notice(w, "pull-comment", "Failed to create comment.")
···
}
func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "NewPull")
+
defer span.End()
+
+
user := s.auth.GetUser(r.WithContext(ctx))
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
return
}
switch r.Method {
case http.MethodGet:
+
span.SetAttributes(attribute.String("method", "GET"))
+
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
+
span.RecordError(err)
s.pages.Error503(w)
return
}
···
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
+
span.RecordError(err)
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
+
span.RecordError(err)
return
}
s.pages.RepoNewPull(w, pages.RepoNewPullParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
Branches: result.Branches,
})
case http.MethodPost:
+
span.SetAttributes(attribute.String("method", "POST"))
+
title := r.FormValue("title")
body := r.FormValue("body")
targetBranch := r.FormValue("targetBranch")
fromFork := r.FormValue("fork")
sourceBranch := r.FormValue("sourceBranch")
patch := r.FormValue("patch")
+
+
span.SetAttributes(
+
attribute.String("targetBranch", targetBranch),
+
attribute.String("sourceBranch", sourceBranch),
+
attribute.Bool("hasFork", fromFork != ""),
+
attribute.Bool("hasPatch", patch != ""),
+
)
if targetBranch == "" {
s.pages.Notice(w, "pull", "Target branch is required.")
+
span.SetAttributes(attribute.String("error", "missing_target_branch"))
return
}
// Determine PR type based on input parameters
-
isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed()
+
isPushAllowed := f.RepoInfo(ctx, s, user).Roles.IsPushAllowed()
isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == ""
isForkBased := fromFork != "" && sourceBranch != ""
isPatchBased := patch != "" && !isBranchBased && !isForkBased
+
span.SetAttributes(
+
attribute.Bool("isPushAllowed", isPushAllowed),
+
attribute.Bool("isBranchBased", isBranchBased),
+
attribute.Bool("isForkBased", isForkBased),
+
attribute.Bool("isPatchBased", isPatchBased),
+
)
+
if isPatchBased && !patchutil.IsFormatPatch(patch) {
if title == "" {
s.pages.Notice(w, "pull", "Title is required for git-diff patches.")
+
span.SetAttributes(attribute.String("error", "missing_title_for_git_diff"))
return
}
}
···
// Validate we have at least one valid PR creation method
if !isBranchBased && !isPatchBased && !isForkBased {
s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.")
+
span.SetAttributes(attribute.String("error", "no_valid_pr_method"))
return
}
// Can't mix branch-based and patch-based approaches
if isBranchBased && patch != "" {
s.pages.Notice(w, "pull", "Cannot select both patch and source branch.")
+
span.SetAttributes(attribute.String("error", "mixed_pr_methods"))
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client to %s: %v", f.Knot, err)
+
span.RecordError(err)
s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")
return
}
···
caps, err := us.Capabilities()
if err != nil {
log.Println("error fetching knot caps", f.Knot, err)
+
span.RecordError(err)
s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")
return
}
+
span.SetAttributes(
+
attribute.Bool("caps.pullRequests.formatPatch", caps.PullRequests.FormatPatch),
+
attribute.Bool("caps.pullRequests.branchSubmissions", caps.PullRequests.BranchSubmissions),
+
attribute.Bool("caps.pullRequests.forkSubmissions", caps.PullRequests.ForkSubmissions),
+
attribute.Bool("caps.pullRequests.patchSubmissions", caps.PullRequests.PatchSubmissions),
+
)
+
if !caps.PullRequests.FormatPatch {
s.pages.Notice(w, "pull", "This knot doesn't support format-patch. Unfortunately, there is no fallback for now.")
+
span.SetAttributes(attribute.String("error", "formatpatch_not_supported"))
return
}
···
if isBranchBased {
if !caps.PullRequests.BranchSubmissions {
s.pages.Notice(w, "pull", "This knot doesn't support branch-based pull requests. Try another way?")
+
span.SetAttributes(attribute.String("error", "branch_submissions_not_supported"))
return
}
-
s.handleBranchBasedPull(w, r, f, user, title, body, targetBranch, sourceBranch)
+
s.handleBranchBasedPull(w, r.WithContext(ctx), f, user, title, body, targetBranch, sourceBranch)
} else if isForkBased {
if !caps.PullRequests.ForkSubmissions {
s.pages.Notice(w, "pull", "This knot doesn't support fork-based pull requests. Try another way?")
+
span.SetAttributes(attribute.String("error", "fork_submissions_not_supported"))
return
}
-
s.handleForkBasedPull(w, r, f, user, fromFork, title, body, targetBranch, sourceBranch)
+
s.handleForkBasedPull(w, r.WithContext(ctx), f, user, fromFork, title, body, targetBranch, sourceBranch)
} else if isPatchBased {
if !caps.PullRequests.PatchSubmissions {
s.pages.Notice(w, "pull", "This knot doesn't support patch-based pull requests. Send your patch over email.")
+
span.SetAttributes(attribute.String("error", "patch_submissions_not_supported"))
return
}
-
s.handlePatchBasedPull(w, r, f, user, title, body, targetBranch, patch)
+
s.handlePatchBasedPull(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch)
}
return
}
}
func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, sourceBranch string) {
+
ctx, span := s.t.TraceStart(r.Context(), "handleBranchBasedPull")
+
defer span.End()
+
+
span.SetAttributes(
+
attribute.String("targetBranch", targetBranch),
+
attribute.String("sourceBranch", sourceBranch),
+
)
+
pullSource := &db.PullSource{
Branch: sourceBranch,
}
···
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "client_creation_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch)
if err != nil {
log.Println("failed to compare", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "comparison_failed"))
s.pages.Notice(w, "pull", err.Error())
return
}
···
sourceRev := comparison.Rev2
patch := comparison.Patch
+
span.SetAttributes(attribute.String("sourceRev", sourceRev))
+
if !patchutil.IsPatchValid(patch) {
+
span.SetAttributes(attribute.String("error", "invalid_patch_format"))
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
return
}
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource)
+
s.createPullRequest(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource)
}
func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) {
+
ctx, span := s.t.TraceStart(r.Context(), "handlePatchBasedPull")
+
defer span.End()
+
+
span.SetAttributes(attribute.String("targetBranch", targetBranch))
+
if !patchutil.IsPatchValid(patch) {
+
span.SetAttributes(attribute.String("error", "invalid_patch_format"))
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
return
}
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil)
+
s.createPullRequest(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch, "", nil, nil)
}
func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) {
-
fork, err := db.GetForkByDid(s.db, user.Did, forkRepo)
+
ctx, span := s.t.TraceStart(r.Context(), "handleForkBasedPull")
+
defer span.End()
+
+
span.SetAttributes(
+
attribute.String("forkRepo", forkRepo),
+
attribute.String("targetBranch", targetBranch),
+
attribute.String("sourceBranch", sourceBranch),
+
)
+
+
fork, err := db.GetForkByDid(ctx, s.db, user.Did, forkRepo)
if errors.Is(err, sql.ErrNoRows) {
+
span.SetAttributes(attribute.String("error", "fork_not_found"))
s.pages.Notice(w, "pull", "No such fork.")
return
} else if err != nil {
log.Println("failed to fetch fork:", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "fork_fetch_failed"))
s.pages.Notice(w, "pull", "Failed to fetch fork.")
return
}
···
secret, err := db.GetRegistrationKey(s.db, fork.Knot)
if err != nil {
log.Println("failed to fetch registration key:", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "registration_key_fetch_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev)
if err != nil {
log.Println("failed to create signed client:", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "signed_client_creation_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
us, err := NewUnsignedClient(fork.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client:", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "unsigned_client_creation_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
resp, err := sc.NewHiddenRef(user.Did, fork.Name, sourceBranch, targetBranch)
if err != nil {
log.Println("failed to create hidden ref:", err, resp.StatusCode)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "hidden_ref_creation_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
switch resp.StatusCode {
case 404:
+
span.SetAttributes(attribute.String("error", "not_found_status"))
case 400:
+
span.SetAttributes(attribute.String("error", "bad_request_status"))
s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
return
}
hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch)
+
span.SetAttributes(attribute.String("hiddenRef", hiddenRef))
+
// We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking
// the targetBranch on the target repository. This code is a bit confusing, but here's an example:
// hiddenRef: hidden/feature-1/main (on repo-fork)
···
comparison, err := us.Compare(user.Did, fork.Name, hiddenRef, sourceBranch)
if err != nil {
log.Println("failed to compare across branches", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "branch_comparison_failed"))
s.pages.Notice(w, "pull", err.Error())
return
}
sourceRev := comparison.Rev2
patch := comparison.Patch
+
span.SetAttributes(attribute.String("sourceRev", sourceRev))
if !patchutil.IsPatchValid(patch) {
+
span.SetAttributes(attribute.String("error", "invalid_patch_format"))
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
return
}
···
forkAtUri, err := syntax.ParseATURI(fork.AtUri)
if err != nil {
log.Println("failed to parse fork AT URI", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "fork_aturi_parse_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{
+
s.createPullRequest(w, r.WithContext(ctx), f, user, title, body, targetBranch, patch, sourceRev, &db.PullSource{
Branch: sourceBranch,
RepoAt: &forkAtUri,
}, &tangled.RepoPull_Source{Branch: sourceBranch, Repo: &fork.AtUri})
···
pullSource *db.PullSource,
recordPullSource *tangled.RepoPull_Source,
) {
-
tx, err := s.db.BeginTx(r.Context(), nil)
+
ctx, span := s.t.TraceStart(r.Context(), "createPullRequest")
+
defer span.End()
+
+
span.SetAttributes(
+
attribute.String("targetBranch", targetBranch),
+
attribute.String("sourceRev", sourceRev),
+
attribute.Bool("hasPullSource", pullSource != nil),
+
)
+
+
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start tx")
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "transaction_start_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
if title == "" {
formatPatches, err := patchutil.ExtractPatches(patch)
if err != nil {
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "extract_patches_failed"))
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err))
return
}
if len(formatPatches) == 0 {
+
span.SetAttributes(attribute.String("error", "no_patches_found"))
s.pages.Notice(w, "pull", "No patches found in the supplied format-patch.")
return
}
title = formatPatches[0].Title
body = formatPatches[0].Body
+
span.SetAttributes(
+
attribute.Bool("title_extracted", true),
+
attribute.Bool("body_extracted", formatPatches[0].Body != ""),
+
)
}
rkey := appview.TID()
···
Patch: patch,
SourceRev: sourceRev,
}
-
err = db.NewPull(tx, &db.Pull{
+
err = db.NewPull(ctx, tx, &db.Pull{
Title: title,
Body: body,
TargetBranch: targetBranch,
···
})
if err != nil {
log.Println("failed to create pull request", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "db_create_pull_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
-
client, _ := s.auth.AuthorizedClient(r)
+
+
client, _ := s.auth.AuthorizedClient(r.WithContext(ctx))
pullId, err := db.NextPullId(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get pull id", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "get_pull_id_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
+
span.SetAttributes(attribute.Int("pullId", pullId))
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullNSID,
Repo: user.Did,
Rkey: rkey,
···
if err != nil {
log.Println("failed to create pull request", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "atproto_create_record_failed"))
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
+
return
+
}
+
+
if err = tx.Commit(); err != nil {
+
log.Println("failed to commit transaction", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "transaction_commit_failed"))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
···
}
func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) {
-
_, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "ValidatePatch")
+
defer span.End()
+
+
_, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "resolve_repo_failed"))
return
}
patch := r.FormValue("patch")
+
span.SetAttributes(attribute.Bool("hasPatch", patch != ""))
+
if patch == "" {
+
span.SetAttributes(attribute.String("error", "empty_patch"))
s.pages.Notice(w, "patch-error", "Patch is required.")
return
}
-
if patch == "" || !patchutil.IsPatchValid(patch) {
+
if !patchutil.IsPatchValid(patch) {
+
span.SetAttributes(attribute.String("error", "invalid_patch_format"))
s.pages.Notice(w, "patch-error", "Invalid patch format. Please provide a valid git diff or format-patch.")
return
}
-
if patchutil.IsFormatPatch(patch) {
+
isFormatPatch := patchutil.IsFormatPatch(patch)
+
span.SetAttributes(attribute.Bool("isFormatPatch", isFormatPatch))
+
+
if isFormatPatch {
s.pages.Notice(w, "patch-preview", "git-format-patch detected. Title and description are optional; if left out, they will be extracted from the first commit.")
} else {
s.pages.Notice(w, "patch-preview", "Regular git-diff detected. Please provide a title and description.")
···
}
func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "PatchUploadFragment")
+
defer span.End()
+
+
user := s.auth.GetUser(r.WithContext(ctx))
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "resolve_repo_failed"))
return
}
s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
})
}
func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) {
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "CompareBranchesFragment")
+
defer span.End()
+
+
user := s.auth.GetUser(r.WithContext(ctx))
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "resolve_repo_failed"))
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "client_creation_failed"))
s.pages.Error503(w)
return
}
···
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "knotserver_connection_failed"))
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "response_read_failed"))
return
}
+
defer resp.Body.Close()
var result types.RepoBranchesResponse
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "response_parse_failed"))
return
}
+
span.SetAttributes(attribute.Int("branches.count", len(result.Branches)))
s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
Branches: result.Branches,
})
}
func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "CompareForksFragment")
+
defer span.End()
+
+
user := s.auth.GetUser(r.WithContext(ctx))
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
return
}
-
forks, err := db.GetForksByDid(s.db, user.Did)
+
forks, err := db.GetForksByDid(ctx, s.db, user.Did)
if err != nil {
log.Println("failed to get forks", err)
+
span.RecordError(err)
return
}
s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
Forks: forks,
})
func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) {
-
user := s.auth.GetUser(r)
+
ctx, span := s.t.TraceStart(r.Context(), "CompareForksBranchesFragment")
+
defer span.End()
+
+
user := s.auth.GetUser(r.WithContext(ctx))
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
return
forkVal := r.URL.Query().Get("fork")
+
span.SetAttributes(attribute.String("fork", forkVal))
// fork repo
-
repo, err := db.GetRepo(s.db, user.Did, forkVal)
+
repo, err := db.GetRepo(ctx, s.db, user.Did, forkVal)
if err != nil {
log.Println("failed to get repo", user.Did, forkVal)
+
span.RecordError(err)
return
sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", repo.Knot)
+
span.RecordError(err)
s.pages.Error503(w)
return
···
sourceResp, err := sourceBranchesClient.Branches(user.Did, repo.Name)
if err != nil {
log.Println("failed to reach knotserver for source branches", err)
+
span.RecordError(err)
return
sourceBody, err := io.ReadAll(sourceResp.Body)
if err != nil {
log.Println("failed to read source response body", err)
+
span.RecordError(err)
return
defer sourceResp.Body.Close()
···
err = json.Unmarshal(sourceBody, &sourceResult)
if err != nil {
log.Println("failed to parse source branches response:", err)
+
span.RecordError(err)
return
targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for target knot %s", f.Knot)
+
span.RecordError(err)
s.pages.Error503(w)
return
···
targetResp, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver for target branches", err)
+
span.RecordError(err)
return
targetBody, err := io.ReadAll(targetResp.Body)
if err != nil {
log.Println("failed to read target response body", err)
+
span.RecordError(err)
return
defer targetResp.Body.Close()
···
err = json.Unmarshal(targetBody, &targetResult)
if err != nil {
log.Println("failed to parse target branches response:", err)
+
span.RecordError(err)
return
s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
SourceBranches: sourceResult.Branches,
TargetBranches: targetResult.Branches,
})
func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) {
-
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "ResubmitPull")
+
defer span.End()
+
+
user := s.auth.GetUser(r.WithContext(ctx))
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
return
-
pull, ok := r.Context().Value("pull").(*db.Pull)
+
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
+
span.RecordError(errors.New("failed to get pull from context"))
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
+
span.SetAttributes(
+
attribute.Int("pull.id", pull.PullId),
+
attribute.String("pull.owner", pull.OwnerDid),
+
attribute.String("method", r.Method),
+
)
+
switch r.Method {
case http.MethodGet:
s.pages.PullResubmitFragment(w, pages.PullResubmitParams{
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
Pull: pull,
})
return
case http.MethodPost:
if pull.IsPatchBased() {
-
s.resubmitPatch(w, r)
+
span.SetAttributes(attribute.String("pull.type", "patch_based"))
+
s.resubmitPatch(w, r.WithContext(ctx))
return
} else if pull.IsBranchBased() {
-
s.resubmitBranch(w, r)
+
span.SetAttributes(attribute.String("pull.type", "branch_based"))
+
s.resubmitBranch(w, r.WithContext(ctx))
return
} else if pull.IsForkBased() {
-
s.resubmitFork(w, r)
+
span.SetAttributes(attribute.String("pull.type", "fork_based"))
+
s.resubmitFork(w, r.WithContext(ctx))
return
+
span.SetAttributes(attribute.String("pull.type", "unknown"))
func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) {
-
user := s.auth.GetUser(r)
+
ctx, span := s.t.TraceStart(r.Context(), "resubmitPatch")
+
defer span.End()
-
pull, ok := r.Context().Value("pull").(*db.Pull)
+
user := s.auth.GetUser(r.WithContext(ctx))
+
+
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
+
span.RecordError(errors.New("failed to get pull from context"))
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
-
f, err := s.fullyResolvedRepo(r)
+
span.SetAttributes(
+
attribute.Int("pull.id", pull.PullId),
+
attribute.String("pull.owner", pull.OwnerDid),
+
)
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
return
if user.Did != pull.OwnerDid {
log.Println("unauthorized user")
+
span.SetAttributes(attribute.String("error", "unauthorized_user"))
w.WriteHeader(http.StatusUnauthorized)
return
patch := r.FormValue("patch")
+
span.SetAttributes(attribute.Bool("has_patch", patch != ""))
if err = validateResubmittedPatch(pull, patch); err != nil {
+
span.SetAttributes(attribute.String("error", "invalid_patch"))
s.pages.Notice(w, "resubmit-error", err.Error())
return
-
tx, err := s.db.BeginTx(r.Context(), nil)
+
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start tx")
+
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
···
err = db.ResubmitPull(tx, pull, patch, "")
if err != nil {
log.Println("failed to resubmit pull request", err)
+
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.")
return
-
client, _ := s.auth.AuthorizedClient(r)
+
client, _ := s.auth.AuthorizedClient(r.WithContext(ctx))
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
+
ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
if err != nil {
// failed to get record
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "record_not_found"))
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
return
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullNSID,
Repo: user.Did,
Rkey: pull.Rkey,
···
})
if err != nil {
log.Println("failed to update record", err)
+
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
return
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
+
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
return
···
func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) {
-
user := s.auth.GetUser(r)
+
ctx, span := s.t.TraceStart(r.Context(), "resubmitBranch")
+
defer span.End()
+
+
user := s.auth.GetUser(r.WithContext(ctx))
-
pull, ok := r.Context().Value("pull").(*db.Pull)
+
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
-
s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
+
span.RecordError(errors.New("failed to get pull from context"))
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
-
f, err := s.fullyResolvedRepo(r)
+
span.SetAttributes(
+
attribute.Int("pull.id", pull.PullId),
+
attribute.String("pull.owner", pull.OwnerDid),
+
attribute.String("pull.source_branch", pull.PullSource.Branch),
+
attribute.String("pull.target_branch", pull.TargetBranch),
+
)
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
return
if user.Did != pull.OwnerDid {
log.Println("unauthorized user")
+
span.SetAttributes(attribute.String("error", "unauthorized_user"))
w.WriteHeader(http.StatusUnauthorized)
return
-
if !f.RepoInfo(s, user).Roles.IsPushAllowed() {
+
if !f.RepoInfo(ctx, s, user).Roles.IsPushAllowed() {
log.Println("unauthorized user")
+
span.SetAttributes(attribute.String("error", "push_not_allowed"))
w.WriteHeader(http.StatusUnauthorized)
return
···
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create client for %s: %s", f.Knot, err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "client_creation_failed"))
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
···
comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch)
if err != nil {
log.Printf("compare request failed: %s", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "compare_failed"))
s.pages.Notice(w, "resubmit-error", err.Error())
return
sourceRev := comparison.Rev2
patch := comparison.Patch
+
span.SetAttributes(attribute.String("source_rev", sourceRev))
if err = validateResubmittedPatch(pull, patch); err != nil {
+
span.SetAttributes(attribute.String("error", "invalid_patch"))
s.pages.Notice(w, "resubmit-error", err.Error())
return
if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev {
+
span.SetAttributes(attribute.String("error", "no_changes"))
s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.")
return
-
tx, err := s.db.BeginTx(r.Context(), nil)
+
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start tx")
+
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
···
err = db.ResubmitPull(tx, pull, patch, sourceRev)
if err != nil {
log.Println("failed to create pull request", err)
+
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
-
client, _ := s.auth.AuthorizedClient(r)
+
client, _ := s.auth.AuthorizedClient(r.WithContext(ctx))
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
+
ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
if err != nil {
// failed to get record
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "record_not_found"))
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
return
···
recordPullSource := &tangled.RepoPull_Source{
Branch: pull.PullSource.Branch,
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullNSID,
Repo: user.Did,
Rkey: pull.Rkey,
···
})
if err != nil {
log.Println("failed to update record", err)
+
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
return
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
+
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
return
···
func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) {
-
user := s.auth.GetUser(r)
+
ctx, span := s.t.TraceStart(r.Context(), "resubmitFork")
+
defer span.End()
-
pull, ok := r.Context().Value("pull").(*db.Pull)
+
user := s.auth.GetUser(r.WithContext(ctx))
+
+
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
-
s.pages.Notice(w, "resubmit-error", "Failed to edit patch. Try again later.")
+
span.RecordError(errors.New("failed to get pull from context"))
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
-
f, err := s.fullyResolvedRepo(r)
+
span.SetAttributes(
+
attribute.Int("pull.id", pull.PullId),
+
attribute.String("pull.owner", pull.OwnerDid),
+
attribute.String("pull.source_branch", pull.PullSource.Branch),
+
attribute.String("pull.target_branch", pull.TargetBranch),
+
)
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
return
if user.Did != pull.OwnerDid {
log.Println("unauthorized user")
+
span.SetAttributes(attribute.String("error", "unauthorized_user"))
w.WriteHeader(http.StatusUnauthorized)
return
-
forkRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String())
+
forkRepo, err := db.GetRepoByAtUri(ctx, s.db, pull.PullSource.RepoAt.String())
if err != nil {
log.Println("failed to get source repo", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "source_repo_not_found"))
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
+
span.SetAttributes(
+
attribute.String("fork.knot", forkRepo.Knot),
+
attribute.String("fork.did", forkRepo.Did),
+
attribute.String("fork.name", forkRepo.Name),
+
)
+
// extract patch by performing compare
ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create client for %s: %s", forkRepo.Knot, err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "client_creation_failed"))
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
···
secret, err := db.GetRegistrationKey(s.db, forkRepo.Knot)
if err != nil {
log.Printf("failed to get registration key for %s: %s", forkRepo.Knot, err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "reg_key_not_found"))
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
···
signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev)
if err != nil {
log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "signed_client_creation_failed"))
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
···
resp, err := signedClient.NewHiddenRef(forkRepo.Did, forkRepo.Name, pull.PullSource.Branch, pull.TargetBranch)
if err != nil || resp.StatusCode != http.StatusNoContent {
log.Printf("failed to update tracking branch: %s", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "hidden_ref_update_failed"))
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch)
+
span.SetAttributes(attribute.String("hidden_ref", hiddenRef))
+
comparison, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch)
if err != nil {
log.Printf("failed to compare branches: %s", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "compare_failed"))
s.pages.Notice(w, "resubmit-error", err.Error())
return
sourceRev := comparison.Rev2
patch := comparison.Patch
+
span.SetAttributes(attribute.String("source_rev", sourceRev))
if err = validateResubmittedPatch(pull, patch); err != nil {
+
span.SetAttributes(attribute.String("error", "invalid_patch"))
s.pages.Notice(w, "resubmit-error", err.Error())
return
if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev {
+
span.SetAttributes(attribute.String("error", "no_changes"))
s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.")
return
-
tx, err := s.db.BeginTx(r.Context(), nil)
+
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start tx")
+
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
···
err = db.ResubmitPull(tx, pull, patch, sourceRev)
if err != nil {
log.Println("failed to create pull request", err)
+
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
return
-
client, _ := s.auth.AuthorizedClient(r)
+
client, _ := s.auth.AuthorizedClient(r.WithContext(ctx))
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
+
ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoPullNSID, user.Did, pull.Rkey)
if err != nil {
// failed to get record
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "record_not_found"))
s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.")
return
···
Branch: pull.PullSource.Branch,
Repo: &repoAt,
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoPullNSID,
Repo: user.Did,
Rkey: pull.Rkey,
···
})
if err != nil {
log.Println("failed to update record", err)
+
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.")
return
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
+
span.RecordError(err)
s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull.")
return
···
func (s *State) MergePull(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "MergePull")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to resolve repo:", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "resolve_repo_failed"))
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
-
pull, ok := r.Context().Value("pull").(*db.Pull)
+
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
+
span.SetAttributes(attribute.String("error", "pull_not_in_context"))
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
+
span.SetAttributes(
+
attribute.Int("pull.id", pull.PullId),
+
attribute.String("pull.owner", pull.OwnerDid),
+
attribute.String("target_branch", pull.TargetBranch),
+
)
+
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no registration key found for domain %s: %s\n", f.Knot, err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "reg_key_not_found"))
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
-
ident, err := s.resolver.ResolveIdent(r.Context(), pull.OwnerDid)
+
ident, err := s.resolver.ResolveIdent(ctx, pull.OwnerDid)
if err != nil {
log.Printf("resolving identity: %s", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "resolve_identity_failed"))
w.WriteHeader(http.StatusNotFound)
return
···
email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid)
if err != nil {
log.Printf("failed to get primary email: %s", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "get_email_failed"))
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err != nil {
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "client_creation_failed"))
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
···
resp, err := ksClient.Merge([]byte(pull.LatestPatch()), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address)
if err != nil {
log.Printf("failed to merge pull request: %s", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "merge_failed"))
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
+
span.SetAttributes(attribute.Int("response.status", resp.StatusCode))
+
if resp.StatusCode == http.StatusOK {
err := db.MergePull(s.db, f.RepoAt, pull.PullId)
if err != nil {
log.Printf("failed to update pull request status in database: %s", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "db_update_failed"))
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId))
} else {
log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode)
+
span.SetAttributes(attribute.String("error", "non_ok_response"))
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {
-
user := s.auth.GetUser(r)
+
ctx, span := s.t.TraceStart(r.Context(), "ClosePull")
+
defer span.End()
-
f, err := s.fullyResolvedRepo(r)
+
user := s.auth.GetUser(r.WithContext(ctx))
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("malformed middleware")
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "resolve_repo_failed"))
return
-
pull, ok := r.Context().Value("pull").(*db.Pull)
+
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
+
span.SetAttributes(attribute.String("error", "pull_not_in_context"))
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
+
span.SetAttributes(
+
attribute.Int("pull.id", pull.PullId),
+
attribute.String("pull.owner", pull.OwnerDid),
+
attribute.String("user.did", user.Did),
+
)
+
// auth filter: only owner or collaborators can close
roles := RolesInRepo(s, user, f)
isCollaborator := roles.IsCollaborator()
isPullAuthor := user.Did == pull.OwnerDid
isCloseAllowed := isCollaborator || isPullAuthor
+
+
span.SetAttributes(
+
attribute.Bool("is_collaborator", isCollaborator),
+
attribute.Bool("is_pull_author", isPullAuthor),
+
attribute.Bool("is_close_allowed", isCloseAllowed),
+
)
+
if !isCloseAllowed {
log.Println("failed to close pull")
+
span.SetAttributes(attribute.String("error", "unauthorized"))
s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
return
// Start a transaction
-
tx, err := s.db.BeginTx(r.Context(), nil)
+
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start transaction", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "transaction_start_failed"))
s.pages.Notice(w, "pull-close", "Failed to close pull.")
return
···
err = db.ClosePull(tx, f.RepoAt, pull.PullId)
if err != nil {
log.Println("failed to close pull", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "db_close_failed"))
s.pages.Notice(w, "pull-close", "Failed to close pull.")
return
···
// Commit the transaction
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "transaction_commit_failed"))
s.pages.Notice(w, "pull-close", "Failed to close pull.")
return
···
func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) {
-
user := s.auth.GetUser(r)
+
ctx, span := s.t.TraceStart(r.Context(), "ReopenPull")
+
defer span.End()
-
f, err := s.fullyResolvedRepo(r)
+
user := s.auth.GetUser(r.WithContext(ctx))
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to resolve repo", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "resolve_repo_failed"))
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
-
pull, ok := r.Context().Value("pull").(*db.Pull)
+
pull, ok := ctx.Value("pull").(*db.Pull)
if !ok {
log.Println("failed to get pull")
+
span.SetAttributes(attribute.String("error", "pull_not_in_context"))
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
-
// auth filter: only owner or collaborators can close
+
span.SetAttributes(
+
attribute.Int("pull.id", pull.PullId),
+
attribute.String("pull.owner", pull.OwnerDid),
+
attribute.String("user.did", user.Did),
+
)
+
+
// auth filter: only owner or collaborators can reopen
roles := RolesInRepo(s, user, f)
isCollaborator := roles.IsCollaborator()
isPullAuthor := user.Did == pull.OwnerDid
-
isCloseAllowed := isCollaborator || isPullAuthor
-
if !isCloseAllowed {
-
log.Println("failed to close pull")
-
s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
+
isReopenAllowed := isCollaborator || isPullAuthor
+
+
span.SetAttributes(
+
attribute.Bool("is_collaborator", isCollaborator),
+
attribute.Bool("is_pull_author", isPullAuthor),
+
attribute.Bool("is_reopen_allowed", isReopenAllowed),
+
)
+
+
if !isReopenAllowed {
+
log.Println("failed to reopen pull")
+
span.SetAttributes(attribute.String("error", "unauthorized"))
+
s.pages.Notice(w, "pull-close", "You are unauthorized to reopen this pull.")
return
// Start a transaction
-
tx, err := s.db.BeginTx(r.Context(), nil)
+
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start transaction", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "transaction_start_failed"))
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
···
err = db.ReopenPull(tx, f.RepoAt, pull.PullId)
if err != nil {
log.Println("failed to reopen pull", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "db_reopen_failed"))
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
···
// Commit the transaction
if err = tx.Commit(); err != nil {
log.Println("failed to commit transaction", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "transaction_commit_failed"))
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
+699 -95
appview/state/repo.go
···
"strings"
"time"
+
"go.opentelemetry.io/otel/attribute"
+
"go.opentelemetry.io/otel/codes"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/auth"
···
)
func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "RepoIndex")
+
defer span.End()
+
ref := chi.URLParam(r, "ref")
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to fully resolve repo", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to fully resolve repo")
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to create unsigned client")
s.pages.Error503(w)
return
}
···
if err != nil {
s.pages.Error503(w)
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
defer resp.Body.Close()
···
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Printf("Error unmarshalling response body: %v", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "error unmarshalling response body")
return
}
···
tagCount := len(result.Tags)
fileCount := len(result.Files)
+
span.SetAttributes(
+
attribute.Int("commits.count", commitCount),
+
attribute.Int("branches.count", branchCount),
+
attribute.Int("tags.count", tagCount),
+
attribute.Int("files.count", fileCount),
+
)
+
commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount)
commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))]
tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))]
···
user := s.auth.GetUser(r)
s.pages.RepoIndexPage(w, pages.RepoIndexParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
TagMap: tagMap,
RepoIndexResponse: result,
CommitsTrunc: commitsTrunc,
···
}
func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "RepoLog")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to fully resolve repo", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to fully resolve repo")
return
}
···
}
ref := chi.URLParam(r, "ref")
+
span.SetAttributes(attribute.Int("page", page), attribute.String("ref", ref))
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to create unsigned client")
return
}
resp, err := us.Log(f.OwnerDid(), f.RepoName, ref, page)
if err != nil {
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("error reading response body: %v", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &repolog)
if err != nil {
log.Println("failed to parse json response", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse json response")
return
}
+
+
span.SetAttributes(attribute.Int("commits.count", len(repolog.Commits)))
result, err := us.Tags(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to reach knotserver for tags")
return
}
···
tagMap[hash] = append(tagMap[hash], tag.Name)
}
+
span.SetAttributes(attribute.Int("tags.count", len(result.Tags)))
+
user := s.auth.GetUser(r)
s.pages.RepoLog(w, pages.RepoLogParams{
LoggedInUser: user,
TagMap: tagMap,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
RepoLogResponse: repolog,
EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)),
})
···
}
func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "RepoDescriptionEdit")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
w.WriteHeader(http.StatusBadRequest)
···
user := s.auth.GetUser(r)
s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
})
return
}
func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "RepoDescription")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve repo")
w.WriteHeader(http.StatusBadRequest)
return
}
···
rkey := repoAt.RecordKey().String()
if rkey == "" {
log.Println("invalid aturi for repo", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "invalid aturi for repo")
w.WriteHeader(http.StatusInternalServerError)
return
}
user := s.auth.GetUser(r)
+
span.SetAttributes(attribute.String("method", r.Method))
switch r.Method {
case http.MethodGet:
s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
})
return
case http.MethodPut:
user := s.auth.GetUser(r)
newDescription := r.FormValue("description")
+
span.SetAttributes(attribute.String("description", newDescription))
client, _ := s.auth.AuthorizedClient(r)
// optimistic update
-
err = db.UpdateDescription(s.db, string(repoAt), newDescription)
+
err = db.UpdateDescription(ctx, s.db, string(repoAt), newDescription)
if err != nil {
-
log.Println("failed to perferom update-description query", err)
+
log.Println("failed to perform update-description query", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to update description in database")
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
return
}
···
// this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field
//
// SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, user.Did, rkey)
+
ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoNSID, user.Did, rkey)
if err != nil {
// failed to get record
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get record from PDS")
s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
return
}
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
+
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: rkey,
···
})
if err != nil {
-
log.Println("failed to perferom update-description query", err)
+
log.Println("failed to perform update-description query", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to put record to PDS")
// failed to get record
s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.")
return
}
-
newRepoInfo := f.RepoInfo(s, user)
+
newRepoInfo := f.RepoInfo(ctx, s, user)
newRepoInfo.Description = newDescription
s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
···
}
func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "RepoCommit")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to fully resolve repo", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to fully resolve repo")
return
}
ref := chi.URLParam(r, "ref")
···
protocol = "https"
}
+
span.SetAttributes(attribute.String("ref", ref), attribute.String("protocol", protocol))
+
if !plumbing.IsHash(ref) {
+
span.SetAttributes(attribute.Bool("invalid_hash", true))
s.pages.Error404(w)
return
}
-
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref))
+
requestURL := fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)
+
span.SetAttributes(attribute.String("request_url", requestURL))
+
+
resp, err := http.Get(requestURL)
if err != nil {
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse response")
return
}
user := s.auth.GetUser(r)
s.pages.RepoCommit(w, pages.RepoCommitParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
RepoCommitResponse: result,
EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}),
})
···
}
func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "RepoTree")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to fully resolve repo", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to fully resolve repo")
return
}
···
if !s.config.Dev {
protocol = "https"
}
-
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
+
+
span.SetAttributes(
+
attribute.String("ref", ref),
+
attribute.String("tree_path", treePath),
+
attribute.String("protocol", protocol),
+
)
+
+
requestURL := fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)
+
span.SetAttributes(attribute.String("request_url", requestURL))
+
+
resp, err := http.Get(requestURL)
if err != nil {
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse response")
return
}
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
// so we can safely redirect to the "parent" (which is the same file).
if len(result.Files) == 0 && result.Parent == treePath {
-
http.Redirect(w, r, fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent), http.StatusFound)
+
redirectURL := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent)
+
span.SetAttributes(attribute.String("redirect_url", redirectURL))
+
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
···
BreadCrumbs: breadcrumbs,
BaseTreeLink: baseTreeLink,
BaseBlobLink: baseBlobLink,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
RepoTreeResponse: result,
})
return
}
func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "RepoTags")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to create unsigned client")
return
}
result, err := us.Tags(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
+
span.SetAttributes(attribute.Int("tags.count", len(result.Tags)))
+
artifacts, err := db.GetArtifact(s.db, db.Filter("repo_at", f.RepoAt))
if err != nil {
log.Println("failed grab artifacts", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to grab artifacts")
return
}
+
+
span.SetAttributes(attribute.Int("artifacts.count", len(artifacts)))
// convert artifacts to map for easy UI building
artifactMap := make(map[plumbing.Hash][]db.Artifact)
···
}
}
+
span.SetAttributes(attribute.Int("dangling_artifacts.count", len(danglingArtifacts)))
+
user := s.auth.GetUser(r)
s.pages.RepoTags(w, pages.RepoTagsParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
RepoTagsResponse: *result,
ArtifactMap: artifactMap,
DanglingArtifacts: danglingArtifacts,
···
}
func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "RepoBranches")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to create unsigned client")
return
}
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse response")
return
}
+
+
span.SetAttributes(attribute.Int("branches.count", len(result.Branches)))
slices.SortFunc(result.Branches, func(a, b types.Branch) int {
if a.IsDefault {
···
user := s.auth.GetUser(r)
s.pages.RepoBranches(w, pages.RepoBranchesParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
RepoBranchesResponse: result,
})
return
}
func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "RepoBlob")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
···
if !s.config.Dev {
protocol = "https"
}
-
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
+
+
span.SetAttributes(
+
attribute.String("ref", ref),
+
attribute.String("file_path", filePath),
+
attribute.String("protocol", protocol),
+
)
+
+
requestURL := fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)
+
span.SetAttributes(attribute.String("request_url", requestURL))
+
+
resp, err := http.Get(requestURL)
if err != nil {
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse response")
return
}
···
showRendered = r.URL.Query().Get("code") != "true"
}
+
span.SetAttributes(
+
attribute.Bool("is_binary", result.IsBinary),
+
attribute.Bool("show_rendered", showRendered),
+
attribute.Bool("render_toggle", renderToggle),
+
)
+
user := s.auth.GetUser(r)
s.pages.RepoBlob(w, pages.RepoBlobParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
RepoBlobResponse: result,
BreadCrumbs: breadcrumbs,
ShowRendered: showRendered,
···
}
func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "RepoBlobRaw")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
···
if !s.config.Dev {
protocol = "https"
}
-
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
+
+
span.SetAttributes(
+
attribute.String("ref", ref),
+
attribute.String("file_path", filePath),
+
attribute.String("protocol", protocol),
+
)
+
+
requestURL := fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)
+
span.SetAttributes(attribute.String("request_url", requestURL))
+
+
resp, err := http.Get(requestURL)
if err != nil {
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to reach knotserver")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "error reading response body")
return
}
···
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse response")
return
}
+
+
span.SetAttributes(attribute.Bool("is_binary", result.IsBinary))
if result.IsBinary {
w.Header().Set("Content-Type", "application/octet-stream")
···
}
func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "AddCollaborator")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
collaborator := r.FormValue("collaborator")
if collaborator == "" {
+
span.SetAttributes(attribute.String("error", "malformed_form"))
http.Error(w, "malformed form", http.StatusBadRequest)
return
}
-
collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator)
+
span.SetAttributes(attribute.String("collaborator", collaborator))
+
+
collaboratorIdent, err := s.resolver.ResolveIdent(ctx, collaborator)
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve collaborator")
w.Write([]byte("failed to resolve collaborator did to a handle"))
return
}
log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
+
span.SetAttributes(
+
attribute.String("collaborator_did", collaboratorIdent.DID.String()),
+
attribute.String("collaborator_handle", collaboratorIdent.Handle.String()),
+
)
// TODO: create an atproto record for this
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "no key found for domain")
return
}
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to create signed client")
return
}
ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
if err != nil {
log.Printf("failed to make request to %s: %s", f.Knot, err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to make request to knotserver")
return
}
if ksResp.StatusCode != http.StatusNoContent {
+
span.SetAttributes(attribute.Int("status_code", ksResp.StatusCode))
w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
return
}
-
tx, err := s.db.BeginTx(r.Context(), nil)
+
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start tx")
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to start transaction")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
···
err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to add collaborator to enforcer")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
-
err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
+
err = db.AddCollaborator(ctx, s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to add collaborator to database")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
···
err = tx.Commit()
if err != nil {
log.Println("failed to commit changes", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to commit transaction")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
···
err = s.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to save enforcer policy")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
-
}
func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "DeleteRepo")
+
defer span.End()
+
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
+
span.SetAttributes(
+
attribute.String("repo_name", f.RepoName),
+
attribute.String("knot", f.Knot),
+
attribute.String("owner_did", f.OwnerDid()),
+
)
+
// remove record from pds
xrpcClient, _ := s.auth.AuthorizedClient(r)
repoRkey := f.RepoAt.RecordKey().String()
-
_, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{
+
_, err = comatproto.RepoDeleteRecord(ctx, xrpcClient, &comatproto.RepoDeleteRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: repoRkey,
})
if err != nil {
log.Printf("failed to delete record: %s", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to delete record from PDS")
s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.")
return
}
log.Println("removed repo record ", f.RepoAt.String())
+
span.SetAttributes(attribute.String("repo_at", f.RepoAt.String()))
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "no key found for domain")
return
}
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to create client")
return
}
ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName)
if err != nil {
log.Printf("failed to make request to %s: %s", f.Knot, err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to make request to knotserver")
return
}
+
span.SetAttributes(attribute.Int("ks_status_code", ksResp.StatusCode))
if ksResp.StatusCode != http.StatusNoContent {
log.Println("failed to remove repo from knot, continuing anyway ", f.Knot)
+
span.SetAttributes(attribute.Bool("knot_remove_failed", true))
} else {
log.Println("removed repo from knot ", f.Knot)
+
span.SetAttributes(attribute.Bool("knot_remove_success", true))
}
-
tx, err := s.db.BeginTx(r.Context(), nil)
+
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println("failed to start tx")
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to start transaction")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
···
err = s.enforcer.E.LoadPolicy()
if err != nil {
log.Println("failed to rollback policies")
+
span.RecordError(err)
}
}()
// remove collaborator RBAC
repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get collaborators")
s.pages.Notice(w, "settings-delete", "Failed to remove collaborators")
return
}
+
span.SetAttributes(attribute.Int("collaborators.count", len(repoCollaborators)))
+
for _, c := range repoCollaborators {
did := c[0]
s.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
···
// remove repo RBAC
err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to remove repo RBAC")
s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules")
return
}
// remove repo from db
-
err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName)
+
err = db.RemoveRepo(ctx, tx, f.OwnerDid(), f.RepoName)
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to remove repo from db")
s.pages.Notice(w, "settings-delete", "Failed to update appview")
return
}
···
err = tx.Commit()
if err != nil {
log.Println("failed to commit changes", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to commit transaction")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
···
err = s.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to save policy")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
···
}
func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "SetDefaultBranch")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
branch := r.FormValue("branch")
if branch == "" {
+
span.SetAttributes(attribute.Bool("malformed_form", true))
+
span.SetStatus(codes.Error, "malformed form")
http.Error(w, "malformed form", http.StatusBadRequest)
return
}
+
span.SetAttributes(
+
attribute.String("branch", branch),
+
attribute.String("repo_name", f.RepoName),
+
attribute.String("knot", f.Knot),
+
attribute.String("owner_did", f.OwnerDid()),
+
)
+
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "no key found for domain")
return
}
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to create client")
return
}
ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch)
if err != nil {
log.Printf("failed to make request to %s: %s", f.Knot, err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to make request to knotserver")
return
}
+
span.SetAttributes(attribute.Int("ks_status_code", ksResp.StatusCode))
if ksResp.StatusCode != http.StatusNoContent {
+
span.SetStatus(codes.Error, "failed to set default branch")
s.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.")
return
}
···
}
func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
-
f, err := s.fullyResolvedRepo(r)
+
ctx, span := s.t.TraceStart(r.Context(), "RepoSettings")
+
defer span.End()
+
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get repo and knot")
return
}
+
span.SetAttributes(
+
attribute.String("repo_name", f.RepoName),
+
attribute.String("knot", f.Knot),
+
attribute.String("owner_did", f.OwnerDid()),
+
attribute.String("method", r.Method),
+
)
+
switch r.Method {
case http.MethodGet:
// for now, this is just pubkeys
user := s.auth.GetUser(r)
-
repoCollaborators, err := f.Collaborators(r.Context(), s)
+
repoCollaborators, err := f.Collaborators(ctx, s)
if err != nil {
log.Println("failed to get collaborators", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "failed_to_get_collaborators"))
}
+
span.SetAttributes(attribute.Int("collaborators.count", len(repoCollaborators)))
isCollaboratorInviteAllowed := false
if user != nil {
···
isCollaboratorInviteAllowed = true
}
}
+
span.SetAttributes(attribute.Bool("invite_allowed", isCollaboratorInviteAllowed))
var branchNames []string
var defaultBranch string
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "failed_to_create_unsigned_client"))
} else {
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "failed_to_reach_knotserver_for_branches"))
} else {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response body: %v", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "failed_to_read_branches_response"))
} else {
var result types.RepoBranchesResponse
err = json.Unmarshal(body, &result)
if err != nil {
log.Println("failed to parse response:", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "failed_to_parse_branches_response"))
} else {
for _, branch := range result.Branches {
branchNames = append(branchNames, branch.Name)
}
+
span.SetAttributes(attribute.Int("branches.count", len(branchNames)))
}
}
}
···
defaultBranchResp, err := us.DefaultBranch(f.OwnerDid(), f.RepoName)
if err != nil {
log.Println("failed to reach knotserver", err)
+
span.RecordError(err)
+
span.SetAttributes(attribute.String("error", "failed_to_reach_knotserver_for_default_branch"))
} else {
defaultBranch = defaultBranchResp.Branch
+
span.SetAttributes(attribute.String("default_branch", defaultBranch))
}
}
s.pages.RepoSettings(w, pages.RepoSettingsParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
Collaborators: repoCollaborators,
IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
Branches: branchNames,
···
return collaborators, nil
-
func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) repoinfo.RepoInfo {
+
func (f *FullyResolvedRepo) RepoInfo(ctx context.Context, s *State, u *auth.User) repoinfo.RepoInfo {
+
ctx, span := s.t.TraceStart(ctx, "RepoInfo")
+
defer span.End()
+
isStarred := false
if u != nil {
isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt))
+
span.SetAttributes(attribute.Bool("is_starred", isStarred))
starCount, err := db.GetStarCount(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get star count for ", f.RepoAt)
+
span.RecordError(err)
+
issueCount, err := db.GetIssueCount(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get issue count for ", f.RepoAt)
+
span.RecordError(err)
+
pullCount, err := db.GetPullCount(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get issue count for ", f.RepoAt)
+
span.RecordError(err)
-
source, err := db.GetRepoSource(s.db, f.RepoAt)
+
+
span.SetAttributes(
+
attribute.Int("stats.stars", starCount),
+
attribute.Int("stats.issues.open", issueCount.Open),
+
attribute.Int("stats.issues.closed", issueCount.Closed),
+
attribute.Int("stats.pulls.open", pullCount.Open),
+
attribute.Int("stats.pulls.closed", pullCount.Closed),
+
attribute.Int("stats.pulls.merged", pullCount.Merged),
+
)
+
+
source, err := db.GetRepoSource(ctx, s.db, f.RepoAt)
if errors.Is(err, sql.ErrNoRows) {
source = ""
} else if err != nil {
log.Println("failed to get repo source for ", f.RepoAt, err)
+
span.RecordError(err)
var sourceRepo *db.Repo
if source != "" {
-
sourceRepo, err = db.GetRepoByAtUri(s.db, source)
+
span.SetAttributes(attribute.String("source", source))
+
sourceRepo, err = db.GetRepoByAtUri(ctx, s.db, source)
if err != nil {
log.Println("failed to get repo by at uri", err)
+
span.RecordError(err)
var sourceHandle *identity.Identity
if sourceRepo != nil {
-
sourceHandle, err = s.resolver.ResolveIdent(context.Background(), sourceRepo.Did)
+
sourceHandle, err = s.resolver.ResolveIdent(ctx, sourceRepo.Did)
if err != nil {
log.Println("failed to resolve source repo", err)
+
span.RecordError(err)
+
} else if sourceHandle != nil {
+
span.SetAttributes(attribute.String("source_handle", sourceHandle.Handle.String()))
knot := f.Knot
+
span.SetAttributes(attribute.String("knot", knot))
+
var disableFork bool
us, err := NewUnsignedClient(knot, s.config.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s: %v", knot, err)
+
span.RecordError(err)
} else {
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err)
+
span.RecordError(err)
} else {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("error reading branch response body: %v", err)
+
span.RecordError(err)
} else {
var branchesResp types.RepoBranchesResponse
if err := json.Unmarshal(body, &branchesResp); err != nil {
log.Printf("error parsing branch response: %v", err)
+
span.RecordError(err)
} else {
disableFork = false
···
if len(branchesResp.Branches) == 0 {
disableFork = true
+
span.SetAttributes(
+
attribute.Int("branches.count", len(branchesResp.Branches)),
+
attribute.Bool("disable_fork", disableFork),
+
)
···
func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "RepoSingleIssue")
+
defer span.End()
+
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve repo")
return
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse issue id")
return
-
issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt)
+
span.SetAttributes(attribute.Int("issue_id", issueIdInt))
+
+
issue, comments, err := db.GetIssueWithComments(ctx, s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue and comments", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get issue and comments")
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
-
issueOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), issue.OwnerDid)
+
span.SetAttributes(
+
attribute.Int("comments.count", len(comments)),
+
attribute.String("issue.title", issue.Title),
+
attribute.String("issue.owner_did", issue.OwnerDid),
+
)
+
+
issueOwnerIdent, err := s.resolver.ResolveIdent(ctx, issue.OwnerDid)
if err != nil {
log.Println("failed to resolve issue owner", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve issue owner")
identsToResolve := make([]string, len(comments))
for i, comment := range comments {
identsToResolve[i] = comment.OwnerDid
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
+
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
Issue: *issue,
Comments: comments,
IssueOwnerHandle: issueOwnerIdent.Handle.String(),
DidHandleMap: didHandleMap,
})
-
func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "CloseIssue")
+
defer span.End()
+
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve repo")
return
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse issue id")
return
-
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
+
span.SetAttributes(attribute.Int("issue_id", issueIdInt))
+
+
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
-
collaborators, err := f.Collaborators(r.Context(), s)
+
collaborators, err := f.Collaborators(ctx, s)
if err != nil {
log.Println("failed to fetch repo collaborators: %w", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to fetch repo collaborators")
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
return user.Did == collab.Did
})
isIssueOwner := user.Did == issue.OwnerDid
+
span.SetAttributes(
+
attribute.Bool("is_collaborator", isCollaborator),
+
attribute.Bool("is_issue_owner", isIssueOwner),
+
)
+
// TODO: make this more granular
if isIssueOwner || isCollaborator {
-
closed := tangled.RepoIssueStateClosed
client, _ := s.auth.AuthorizedClient(r)
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueStateNSID,
Repo: user.Did,
Rkey: appview.TID(),
···
if err != nil {
log.Println("failed to update issue state", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to update issue state in PDS")
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
···
err := db.CloseIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to close issue", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to close issue in database")
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
···
return
} else {
log.Println("user is not permitted to close issue")
+
span.SetAttributes(attribute.Bool("permission_denied", true))
http.Error(w, "for biden", http.StatusUnauthorized)
return
func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "ReopenIssue")
+
defer span.End()
+
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve repo")
return
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse issue id")
return
-
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
+
span.SetAttributes(attribute.Int("issue_id", issueIdInt))
+
+
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
-
collaborators, err := f.Collaborators(r.Context(), s)
+
collaborators, err := f.Collaborators(ctx, s)
if err != nil {
log.Println("failed to fetch repo collaborators: %w", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to fetch repo collaborators")
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
return user.Did == collab.Did
})
isIssueOwner := user.Did == issue.OwnerDid
+
+
span.SetAttributes(
+
attribute.Bool("is_collaborator", isCollaborator),
+
attribute.Bool("is_issue_owner", isIssueOwner),
+
)
if isCollaborator || isIssueOwner {
err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to reopen issue", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to reopen issue")
s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
return
···
return
} else {
log.Println("user is not the owner of the repo")
+
span.SetAttributes(attribute.Bool("permission_denied", true))
http.Error(w, "forbidden", http.StatusUnauthorized)
return
func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "NewIssueComment")
+
defer span.End()
+
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve repo")
return
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse issue id")
return
+
span.SetAttributes(
+
attribute.Int("issue_id", issueIdInt),
+
attribute.String("method", r.Method),
+
)
+
switch r.Method {
case http.MethodPost:
body := r.FormValue("body")
if body == "" {
+
span.SetAttributes(attribute.Bool("missing_body", true))
s.pages.Notice(w, "issue", "Body is required")
return
···
commentId := mathrand.IntN(1000000)
rkey := appview.TID()
+
span.SetAttributes(
+
attribute.Int("comment_id", commentId),
+
attribute.String("rkey", rkey),
+
)
+
err := db.NewIssueComment(s.db, &db.Comment{
OwnerDid: user.Did,
RepoAt: f.RepoAt,
···
})
if err != nil {
log.Println("failed to create comment", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to create comment in database")
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
···
issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue at", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get issue at")
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
+
span.SetAttributes(attribute.String("issue_at", issueAt))
+
atUri := f.RepoAt.String()
client, _ := s.auth.AuthorizedClient(r)
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueCommentNSID,
Repo: user.Did,
Rkey: rkey,
···
})
if err != nil {
log.Println("failed to create comment", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to create comment in PDS")
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
···
func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "IssueComment")
+
defer span.End()
+
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve repo")
return
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse issue id")
return
···
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse comment id")
return
-
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
+
span.SetAttributes(
+
attribute.Int("issue_id", issueIdInt),
+
attribute.Int("comment_id", commentIdInt),
+
)
+
+
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
···
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get comment")
return
-
identity, err := s.resolver.ResolveIdent(r.Context(), comment.OwnerDid)
+
identity, err := s.resolver.ResolveIdent(ctx, comment.OwnerDid)
if err != nil {
log.Println("failed to resolve did")
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve did")
return
···
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
DidHandleMap: didHandleMap,
Issue: issue,
Comment: comment,
···
func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "EditIssueComment")
+
defer span.End()
+
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve repo")
return
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse issue id")
return
···
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse comment id")
return
-
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
+
span.SetAttributes(
+
attribute.Int("issue_id", issueIdInt),
+
attribute.Int("comment_id", commentIdInt),
+
attribute.String("method", r.Method),
+
)
+
+
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
···
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get comment")
return
if comment.OwnerDid != user.Did {
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
+
span.SetAttributes(attribute.Bool("permission_denied", true))
return
···
case http.MethodGet:
s.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
Issue: issue,
Comment: comment,
})
···
client, _ := s.auth.AuthorizedClient(r)
rkey := comment.Rkey
+
span.SetAttributes(
+
attribute.String("new_body", newBody),
+
attribute.String("rkey", rkey),
+
)
+
// optimistic update
edited := time.Now()
err = db.EditComment(s.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody)
if err != nil {
log.Println("failed to perferom update-description query", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to edit comment in database")
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
return
···
// rkey is optional, it was introduced later
if comment.Rkey != "" {
// update the record on pds
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, rkey)
+
ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoIssueCommentNSID, user.Did, rkey)
if err != nil {
// failed to get record
log.Println(err, rkey)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get record from PDS")
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
return
···
createdAt := record["createdAt"].(string)
commentIdInt64 := int64(commentIdInt)
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueCommentNSID,
Repo: user.Did,
Rkey: rkey,
···
})
if err != nil {
log.Println(err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to put record to PDS")
···
// return new comment body with htmx
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
DidHandleMap: didHandleMap,
Issue: issue,
Comment: comment,
})
return
-
-
func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "DeleteIssueComment")
+
defer span.End()
+
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve repo")
return
···
if err != nil {
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse issue id")
return
-
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
+
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
···
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to parse comment id")
return
+
span.SetAttributes(
+
attribute.Int("issue_id", issueIdInt),
+
attribute.Int("comment_id", commentIdInt),
+
)
+
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get comment")
return
if comment.OwnerDid != user.Did {
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
+
span.SetAttributes(attribute.Bool("permission_denied", true))
return
if comment.Deleted != nil {
http.Error(w, "comment already deleted", http.StatusBadRequest)
+
span.SetAttributes(attribute.Bool("already_deleted", true))
return
···
err = db.DeleteComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
log.Println("failed to delete comment")
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to delete comment in database")
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
return
···
// delete from pds
if comment.Rkey != "" {
client, _ := s.auth.AuthorizedClient(r)
-
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
+
_, err = comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{
Collection: tangled.GraphFollowNSID,
Repo: user.Did,
Rkey: comment.Rkey,
})
if err != nil {
log.Println(err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to delete record from PDS")
···
// htmx fragment of comment after deletion
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
DidHandleMap: didHandleMap,
Issue: issue,
Comment: comment,
···
func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "RepoIssues")
+
defer span.End()
+
params := r.URL.Query()
state := params.Get("state")
isOpen := true
···
isOpen = true
+
span.SetAttributes(
+
attribute.Bool("is_open", isOpen),
+
attribute.String("state_param", state),
+
)
+
page, ok := r.Context().Value("page").(pagination.Page)
if !ok {
log.Println("failed to get page")
+
span.SetAttributes(attribute.Bool("page_not_found", true))
page = pagination.FirstPage()
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve repo")
return
-
issues, err := db.GetIssues(s.db, f.RepoAt, isOpen, page)
+
issues, err := db.GetIssues(ctx, s.db, f.RepoAt, isOpen, page)
if err != nil {
log.Println("failed to get issues", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get issues")
s.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
return
+
span.SetAttributes(attribute.Int("issues.count", len(issues)))
+
identsToResolve := make([]string, len(issues))
for i, issue := range issues {
identsToResolve[i] = issue.OwnerDid
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
+
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
s.pages.RepoIssues(w, pages.RepoIssuesParams{
LoggedInUser: s.auth.GetUser(r),
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
Issues: issues,
DidHandleMap: didHandleMap,
FilteringByOpen: isOpen,
···
func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "NewIssue")
+
defer span.End()
+
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Println("failed to get repo and knot", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve repo")
return
+
+
span.SetAttributes(attribute.String("method", r.Method))
switch r.Method {
case http.MethodGet:
s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
})
case http.MethodPost:
title := r.FormValue("title")
body := r.FormValue("body")
+
span.SetAttributes(
+
attribute.String("title", title),
+
attribute.String("body_length", fmt.Sprintf("%d", len(body))),
+
)
+
if title == "" || body == "" {
+
span.SetAttributes(attribute.Bool("form_validation_failed", true))
s.pages.Notice(w, "issues", "Title and body are required")
return
-
tx, err := s.db.BeginTx(r.Context(), nil)
+
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to begin transaction")
s.pages.Notice(w, "issues", "Failed to create issue, try again later")
return
···
})
if err != nil {
log.Println("failed to create issue", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to create issue in database")
s.pages.Notice(w, "issues", "Failed to create issue.")
return
···
issueId, err := db.GetIssueId(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get issue id", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get issue id")
s.pages.Notice(w, "issues", "Failed to create issue.")
return
+
span.SetAttributes(attribute.Int("issue_id", issueId))
+
client, _ := s.auth.AuthorizedClient(r)
atUri := f.RepoAt.String()
-
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
rkey := appview.TID()
+
span.SetAttributes(attribute.String("rkey", rkey))
+
+
resp, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueNSID,
Repo: user.Did,
-
Rkey: appview.TID(),
+
Rkey: rkey,
Record: &lexutil.LexiconTypeDecoder{
Val: &tangled.RepoIssue{
Repo: atUri,
···
})
if err != nil {
log.Println("failed to create issue", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to create issue in PDS")
s.pages.Notice(w, "issues", "Failed to create issue.")
return
+
span.SetAttributes(attribute.String("issue_uri", resp.Uri))
+
err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri)
if err != nil {
log.Println("failed to set issue at", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to set issue URI in database")
s.pages.Notice(w, "issues", "Failed to create issue.")
return
···
func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "ForkRepo")
+
defer span.End()
+
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r)
+
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
if err != nil {
log.Printf("failed to resolve source repo: %v", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to resolve source repo")
return
+
span.SetAttributes(
+
attribute.String("method", r.Method),
+
attribute.String("repo_name", f.RepoName),
+
attribute.String("owner_did", f.OwnerDid()),
+
attribute.String("knot", f.Knot),
+
)
+
switch r.Method {
case http.MethodGet:
user := s.auth.GetUser(r)
knots, err := s.enforcer.GetDomainsForUser(user.Did)
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get domains for user")
s.pages.Notice(w, "repo", "Invalid user account.")
return
+
span.SetAttributes(attribute.Int("knots.count", len(knots)))
+
s.pages.ForkRepo(w, pages.ForkRepoParams{
LoggedInUser: user,
Knots: knots,
-
RepoInfo: f.RepoInfo(s, user),
+
RepoInfo: f.RepoInfo(ctx, s, user),
})
case http.MethodPost:
-
knot := r.FormValue("knot")
if knot == "" {
+
span.SetAttributes(attribute.Bool("missing_knot", true))
s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
return
+
span.SetAttributes(attribute.String("target_knot", knot))
+
ok, err := s.enforcer.E.Enforce(user.Did, knot, knot, "repo:create")
if err != nil || !ok {
+
span.SetAttributes(
+
attribute.Bool("permission_denied", true),
+
attribute.Bool("enforce_error", err != nil),
+
)
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
return
forkName := fmt.Sprintf("%s", f.RepoName)
+
span.SetAttributes(attribute.String("fork_name", forkName))
// this check is *only* to see if the forked repo name already exists
// in the user's account.
-
existingRepo, err := db.GetRepo(s.db, user.Did, f.RepoName)
+
existingRepo, err := db.GetRepo(ctx, s.db, user.Did, f.RepoName)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// no existing repo with this name found, we can use the name as is
+
span.SetAttributes(attribute.Bool("repo_name_available", true))
} else {
log.Println("error fetching existing repo from db", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to check for existing repo")
s.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.")
return
} else if existingRepo != nil {
// repo with this name already exists, append random string
forkName = fmt.Sprintf("%s-%s", forkName, randomString(3))
+
span.SetAttributes(
+
attribute.Bool("repo_name_conflict", true),
+
attribute.String("adjusted_fork_name", forkName),
+
)
+
secret, err := db.GetRegistrationKey(s.db, knot)
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to get registration key")
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot))
return
client, err := NewSignedClient(knot, secret, s.config.Dev)
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to create signed client")
s.pages.Notice(w, "repo", "Failed to reach knot server.")
return
···
forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName)
sourceAt := f.RepoAt.String()
+
span.SetAttributes(
+
attribute.String("fork_source_url", forkSourceUrl),
+
attribute.String("source_at", sourceAt),
+
)
+
rkey := appview.TID()
repo := &db.Repo{
Did: user.Did,
···
Source: sourceAt,
-
tx, err := s.db.BeginTx(r.Context(), nil)
+
span.SetAttributes(attribute.String("rkey", rkey))
+
+
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
log.Println(err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to begin transaction")
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
···
err = s.enforcer.E.LoadPolicy()
if err != nil {
log.Println("failed to rollback policies")
+
span.RecordError(err)
}()
resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName)
if err != nil {
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to fork repo on knot server")
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
return
+
+
span.SetAttributes(attribute.Int("fork_response_status", resp.StatusCode))
switch resp.StatusCode {
case http.StatusConflict:
+
span.SetAttributes(attribute.Bool("name_conflict", true))
s.pages.Notice(w, "repo", "A repository with that name already exists.")
return
case http.StatusInternalServerError:
+
span.SetAttributes(attribute.Bool("server_error", true))
s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
+
return
case http.StatusNoContent:
// continue
···
xrpcClient, _ := s.auth.AuthorizedClient(r)
createdAt := time.Now().Format(time.RFC3339)
-
atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
+
atresp, err := comatproto.RepoPutRecord(ctx, xrpcClient, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: rkey,
···
})
if err != nil {
log.Printf("failed to create record: %s", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to create record in PDS")
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
return
log.Println("created repo record: ", atresp.Uri)
+
span.SetAttributes(attribute.String("repo_uri", atresp.Uri))
repo.AtUri = atresp.Uri
-
err = db.AddRepo(tx, repo)
+
err = db.AddRepo(ctx, tx, repo)
if err != nil {
log.Println(err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to add repo to database")
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
···
err = s.enforcer.AddRepo(user.Did, knot, p)
if err != nil {
log.Println(err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to set up repository permissions")
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
return
···
err = tx.Commit()
if err != nil {
log.Println("failed to commit changes", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to commit transaction")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
···
err = s.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
+
span.RecordError(err)
+
span.SetStatus(codes.Error, "failed to save policy")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
+24 -6
appview/state/repo_util.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/go-chi/chi/v5"
"github.com/go-git/go-git/v5/plumbing/object"
+
"go.opentelemetry.io/otel/attribute"
"tangled.sh/tangled.sh/core/appview/auth"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages/repoinfo"
+
"tangled.sh/tangled.sh/core/telemetry"
)
func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
+
ctx := r.Context()
+
+
attrs := telemetry.MapAttrs(map[string]string{
+
"repo": chi.URLParam(r, "repo"),
+
"ref": chi.URLParam(r, "ref"),
+
})
+
+
ctx, span := s.t.TraceStart(ctx, "fullyResolvedRepo", attrs...)
+
defer span.End()
+
repoName := chi.URLParam(r, "repo")
-
knot, ok := r.Context().Value("knot").(string)
+
knot, ok := ctx.Value("knot").(string)
if !ok {
log.Println("malformed middleware")
return nil, fmt.Errorf("malformed middleware")
}
-
id, ok := r.Context().Value("resolvedId").(identity.Identity)
+
+
span.SetAttributes(attribute.String("knot", knot))
+
+
id, ok := ctx.Value("resolvedId").(identity.Identity)
if !ok {
log.Println("malformed middleware")
return nil, fmt.Errorf("malformed middleware")
}
-
repoAt, ok := r.Context().Value("repoAt").(string)
+
span.SetAttributes(attribute.String("did", id.DID.String()))
+
+
repoAt, ok := ctx.Value("repoAt").(string)
if !ok {
log.Println("malformed middleware")
return nil, fmt.Errorf("malformed middleware")
···
}
ref = defaultBranch.Branch
+
+
span.SetAttributes(attribute.String("default_branch", ref))
}
-
// pass through values from the middleware
-
description, ok := r.Context().Value("repoDescription").(string)
-
addedAt, ok := r.Context().Value("repoAddedAt").(string)
+
description, ok := ctx.Value("repoDescription").(string)
+
addedAt, ok := ctx.Value("repoAddedAt").(string)
return &FullyResolvedRepo{
Knot: knot,
+48 -8
appview/state/state.go
···
lexutil "github.com/bluesky-social/indigo/lex/util"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/go-chi/chi/v5"
+
"go.opentelemetry.io/otel/attribute"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/auth"
···
if err != nil {
return nil, fmt.Errorf("failed to create jetstream client: %w", err)
}
-
err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper))
+
err = jc.StartJetstream(ctx, appview.Ingest(wrapper))
if err != nil {
return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
}
···
}
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "Timeline")
+
defer span.End()
+
user := s.auth.GetUser(r)
+
span.SetAttributes(attribute.String("user.did", user.Did))
-
timeline, err := db.MakeTimeline(s.db)
+
timeline, err := db.MakeTimeline(ctx, s.db)
if err != nil {
log.Println(err)
+
span.RecordError(err)
s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
}
···
didsToResolve = append(didsToResolve, ev.Star.StarredByDid, ev.Star.Repo.Did)
}
}
+
span.SetAttributes(attribute.Int("dids.to_resolve.count", len(didsToResolve)))
-
resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
+
resolvedIds := s.resolver.ResolveIdents(ctx, didsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
}
+
span.SetAttributes(attribute.Int("dids.resolved.count", len(resolvedIds)))
s.pages.Timeline(w, pages.TimelineParams{
LoggedInUser: user,
···
}
func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) {
+
ctx, span := s.t.TraceStart(r.Context(), "NewRepo")
+
defer span.End()
+
switch r.Method {
case http.MethodGet:
user := s.auth.GetUser(r)
+
span.SetAttributes(attribute.String("user.did", user.Did))
+
span.SetAttributes(attribute.String("request.method", "GET"))
+
knots, err := s.enforcer.GetDomainsForUser(user.Did)
if err != nil {
+
span.RecordError(err)
s.pages.Notice(w, "repo", "Invalid user account.")
return
}
+
span.SetAttributes(attribute.Int("knots.count", len(knots)))
s.pages.NewRepo(w, pages.NewRepoParams{
LoggedInUser: user,
···
case http.MethodPost:
user := s.auth.GetUser(r)
+
span.SetAttributes(attribute.String("user.did", user.Did))
+
span.SetAttributes(attribute.String("request.method", "POST"))
domain := r.FormValue("domain")
if domain == "" {
s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
return
}
+
span.SetAttributes(attribute.String("domain", domain))
repoName := r.FormValue("name")
if repoName == "" {
s.pages.Notice(w, "repo", "Repository name cannot be empty.")
return
}
+
span.SetAttributes(attribute.String("repo.name", repoName))
// Check for valid repository name (GitHub-like rules)
// No spaces, only alphanumeric characters, dashes, and underscores
···
if defaultBranch == "" {
defaultBranch = "main"
}
+
span.SetAttributes(attribute.String("repo.default_branch", defaultBranch))
description := r.FormValue("description")
ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
if err != nil || !ok {
+
if err != nil {
+
span.RecordError(err)
+
}
+
span.SetAttributes(attribute.Bool("permission.granted", false))
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
return
}
+
span.SetAttributes(attribute.Bool("permission.granted", true))
-
existingRepo, err := db.GetRepo(s.db, user.Did, repoName)
+
existingRepo, err := db.GetRepo(ctx, s.db, user.Did, repoName)
if err == nil && existingRepo != nil {
+
span.SetAttributes(attribute.Bool("repo.exists", true))
s.pages.Notice(w, "repo", fmt.Sprintf("A repo by this name already exists on %s", existingRepo.Knot))
return
}
+
span.SetAttributes(attribute.Bool("repo.exists", false))
secret, err := db.GetRegistrationKey(s.db, domain)
if err != nil {
+
span.RecordError(err)
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain))
return
}
client, err := NewSignedClient(domain, secret, s.config.Dev)
if err != nil {
+
span.RecordError(err)
s.pages.Notice(w, "repo", "Failed to connect to knot server.")
return
}
···
Description: description,
}
-
xrpcClient, _ := s.auth.AuthorizedClient(r)
+
rWithCtx := r.WithContext(ctx)
+
xrpcClient, _ := s.auth.AuthorizedClient(rWithCtx)
createdAt := time.Now().Format(time.RFC3339)
-
atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
+
atresp, err := comatproto.RepoPutRecord(ctx, xrpcClient, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
Repo: user.Did,
Rkey: rkey,
···
}},
})
if err != nil {
+
span.RecordError(err)
log.Printf("failed to create record: %s", err)
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
return
}
log.Println("created repo record: ", atresp.Uri)
+
span.SetAttributes(attribute.String("repo.uri", atresp.Uri))
-
tx, err := s.db.BeginTx(r.Context(), nil)
+
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
+
span.RecordError(err)
log.Println(err)
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
···
resp, err := client.NewRepo(user.Did, repoName, defaultBranch)
if err != nil {
+
span.RecordError(err)
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
return
}
+
span.SetAttributes(attribute.Int("knot_response.status", resp.StatusCode))
switch resp.StatusCode {
case http.StatusConflict:
···
}
repo.AtUri = atresp.Uri
-
err = db.AddRepo(tx, repo)
+
err = db.AddRepo(ctx, tx, repo)
if err != nil {
+
span.RecordError(err)
log.Println(err)
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
···
p, _ := securejoin.SecureJoin(user.Did, repoName)
err = s.enforcer.AddRepo(user.Did, domain, p)
if err != nil {
+
span.RecordError(err)
log.Println(err)
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
return
···
err = tx.Commit()
if err != nil {
+
span.RecordError(err)
log.Println("failed to commit changes", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
···
err = s.enforcer.E.SavePolicy()
if err != nil {
+
span.RecordError(err)
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
+14 -4
telemetry/telemetry.go
···
import (
"context"
+
"fmt"
-
"go.opentelemetry.io/otel"
+
"go.opentelemetry.io/otel/attribute"
otelmetric "go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
···
return t.tracer
}
-
func (t *Telemetry) TraceStart(ctx context.Context, name string) (context.Context, oteltrace.Span) {
-
tracer := otel.Tracer(t.serviceName)
-
return tracer.Start(ctx, name)
+
func (t *Telemetry) TraceStart(ctx context.Context, name string, attrs ...attribute.KeyValue) (context.Context, oteltrace.Span) {
+
ctx, span := t.tracer.Start(ctx, name)
+
span.SetAttributes(attrs...)
+
return ctx, span
+
}
+
+
func MapAttrs[T any](attrs map[string]T) []attribute.KeyValue {
+
var result []attribute.KeyValue
+
for k, v := range attrs {
+
result = append(result, attribute.Key(k).String(fmt.Sprintf("%v", v)))
+
}
+
return result
}