0

Update deps

This commit is contained in:
eleksir
2024-04-19 21:30:16 +03:00
parent 3b6dd3fdef
commit 442142291c
585 changed files with 21099 additions and 15328 deletions

View File

@@ -2,8 +2,6 @@ linters:
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dogsled
- dupl
- errcheck
@@ -24,12 +22,10 @@ linters:
- prealloc
- revive
- staticcheck
- structcheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace
issues:
exclude-rules:

View File

@@ -1,9 +1,87 @@
# Changelog
## 0.27.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.27.0.
### Breaking Changes
- `Exception.ThreadId` is now typed as `uint64`. It was wrongly typed as `string` before. ([#770](https://github.com/getsentry/sentry-go/pull/770))
### Misc
- Export `Event.Attachments` ([#771](https://github.com/getsentry/sentry-go/pull/771))
## 0.26.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.26.0.
### Breaking Changes
As previously announced, this release removes some methods from the SDK.
- `sentry.TransactionName()` use `sentry.WithTransactionName()` instead.
- `sentry.OpName()` use `sentry.WithOpName()` instead.
- `sentry.TransctionSource()` use `sentry.WithTransactionSource()` instead.
- `sentry.SpanSampled()` use `sentry.WithSpanSampled()` instead.
### Features
- Add `WithDescription` span option ([#751](https://github.com/getsentry/sentry-go/pull/751))
```go
span := sentry.StartSpan(ctx, "http.client", WithDescription("GET /api/users"))
```
- Add support for package name parsing in Go 1.20 and higher ([#730](https://github.com/getsentry/sentry-go/pull/730))
### Bug Fixes
- Apply `ClientOptions.SampleRate` only to errors & messages ([#754](https://github.com/getsentry/sentry-go/pull/754))
- Check if git is available before executing any git commands ([#737](https://github.com/getsentry/sentry-go/pull/737))
## 0.25.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.25.0.
### Breaking Changes
As previously announced, this release removes two global constants from the SDK.
- `sentry.Version` was removed. Use `sentry.SDKVersion` instead ([#727](https://github.com/getsentry/sentry-go/pull/727))
- `sentry.SDKIdentifier` was removed. Use `Client.GetSDKIdentifier()` instead ([#727](https://github.com/getsentry/sentry-go/pull/727))
### Features
- Add `ClientOptions.IgnoreTransactions`, which allows you to ignore specific transactions based on their name ([#717](https://github.com/getsentry/sentry-go/pull/717))
- Add `ClientOptions.Tags`, which allows you to set global tags that are applied to all events. You can also define tags by setting `SENTRY_TAGS_` environment variables ([#718](https://github.com/getsentry/sentry-go/pull/718))
### Bug fixes
- Fix an issue in the profiler that would cause an infinite loop if the duration of a transaction is longer than 30 seconds ([#724](https://github.com/getsentry/sentry-go/issues/724))
### Misc
- `dsn.RequestHeaders()` is not to be removed, though it is still considered deprecated and should only be used when using a custom transport that sends events to the `/store` endpoint ([#720](https://github.com/getsentry/sentry-go/pull/720))
## 0.24.1
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.24.1.
### Bug fixes
- Prevent a panic in `sentryotel.flushSpanProcessor()` ([(#711)](https://github.com/getsentry/sentry-go/pull/711))
- Prevent a panic when setting the SDK identifier ([#715](https://github.com/getsentry/sentry-go/pull/715))
## 0.24.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.24.0.
### Deprecations
- `sentry.Version` to be removed in 0.25.0. Use `sentry.SDKVersion` instead.
- `sentry.SDKIdentifier` to be removed in 0.25.0. Use `Client.GetSDKIdentifier()` instead.
- `dsn.RequestHeaders()` to be removed after 0.25.0, but no earlier than December 1, 2023. Requests to the `/envelope` endpoint are authenticated using the DSN in the envelope header.
### Features
- Run a single instance of the profiler instead of multiple ones for each Go routine ([#655](https://github.com/getsentry/sentry-go/pull/655))

View File

@@ -140,6 +140,9 @@ type ClientOptions struct {
// and if applicable, caught errors type and value.
// If the match is found, then a whole event will be dropped.
IgnoreErrors []string
// List of regexp strings that will be used to match against a transaction's
// name. If a match is found, then the transaction will be dropped.
IgnoreTransactions []string
// If this flag is enabled, certain personally identifiable information (PII) is added by active integrations.
// By default, no such data is sent.
SendDefaultPII bool
@@ -221,6 +224,8 @@ type ClientOptions struct {
// is not optimized for long chains either. The top-level error together with a
// stack trace is often the most useful information.
MaxErrorDepth int
// Default event tags. These are overridden by tags set on a scope.
Tags map[string]string
}
// Client is the underlying processor that is used by the main API and Hub
@@ -371,6 +376,8 @@ func (client *Client) setupIntegrations() {
new(environmentIntegration),
new(modulesIntegration),
new(ignoreErrorsIntegration),
new(ignoreTransactionsIntegration),
new(globalTagsIntegration),
}
if client.options.Integrations != nil {
@@ -598,9 +605,9 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
}
// Transactions are sampled by options.TracesSampleRate or
// options.TracesSampler when they are started. All other events
// (errors, messages) are sampled here.
if event.Type != transactionType && !sample(client.options.SampleRate) {
// options.TracesSampler when they are started. Other events
// (errors, messages) are sampled here. Does not apply to check-ins.
if event.Type != transactionType && event.Type != checkInType && !sample(client.options.SampleRate) {
Logger.Println("Event dropped due to SampleRate hit.")
return nil
}
@@ -619,7 +626,7 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
Logger.Println("Transaction dropped due to BeforeSendTransaction callback.")
return nil
}
} else if event.Type != transactionType && client.options.BeforeSend != nil {
} else if event.Type != transactionType && event.Type != checkInType && client.options.BeforeSend != nil {
// All other events
if event = client.options.BeforeSend(event, hint); event == nil {
Logger.Println("Event dropped due to BeforeSend callback.")

View File

@@ -196,11 +196,15 @@ func (dsn Dsn) GetAPIURL() *url.URL {
return parsedURL
}
// RequestHeaders returns all the necessary headers that have to be used in the transport.
// Deprecated: To be removed after 0.25.0, but no earlier than December 1, 2023. Requests to /envelope are authenticated using the DSN in the envelope header itself.
// RequestHeaders returns all the necessary headers that have to be used in the transport when seinding events
// to the /store endpoint.
//
// Deprecated: This method shall only be used if you want to implement your own transport that sends events to
// the /store endpoint. If you're using the transport provided by the SDK, all necessary headers to authenticate
// against the /envelope endpoint are added automatically.
func (dsn Dsn) RequestHeaders() map[string]string {
auth := fmt.Sprintf("Sentry sentry_version=%s, sentry_timestamp=%d, "+
"sentry_client=sentry.go/%s, sentry_key=%s", apiVersion, time.Now().Unix(), Version, dsn.publicKey)
"sentry_client=sentry.go/%s, sentry_key=%s", apiVersion, time.Now().Unix(), SDKVersion, dsn.publicKey)
if dsn.secretKey != "" {
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey)

View File

@@ -2,6 +2,7 @@ package sentry
import (
"fmt"
"os"
"regexp"
"runtime"
"runtime/debug"
@@ -26,7 +27,7 @@ func (mi *modulesIntegration) SetupOnce(client *Client) {
client.AddEventProcessor(mi.processor)
}
func (mi *modulesIntegration) processor(event *Event, hint *EventHint) *Event {
func (mi *modulesIntegration) processor(event *Event, _ *EventHint) *Event {
if len(event.Modules) == 0 {
mi.once.Do(func() {
info, ok := debug.ReadBuildInfo()
@@ -69,7 +70,7 @@ func (ei *environmentIntegration) SetupOnce(client *Client) {
client.AddEventProcessor(ei.processor)
}
func (ei *environmentIntegration) processor(event *Event, hint *EventHint) *Event {
func (ei *environmentIntegration) processor(event *Event, _ *EventHint) *Event {
// Initialize maps as necessary.
contextNames := []string{"device", "os", "runtime"}
if event.Contexts == nil {
@@ -134,7 +135,7 @@ func (iei *ignoreErrorsIntegration) SetupOnce(client *Client) {
client.AddEventProcessor(iei.processor)
}
func (iei *ignoreErrorsIntegration) processor(event *Event, hint *EventHint) *Event {
func (iei *ignoreErrorsIntegration) processor(event *Event, _ *EventHint) *Event {
suspects := getIgnoreErrorsSuspects(event)
for _, suspect := range suspects {
@@ -177,6 +178,40 @@ func getIgnoreErrorsSuspects(event *Event) []string {
return suspects
}
// ================================
// Ignore Transactions Integration
// ================================
type ignoreTransactionsIntegration struct {
ignoreTransactions []*regexp.Regexp
}
func (iei *ignoreTransactionsIntegration) Name() string {
return "IgnoreTransactions"
}
func (iei *ignoreTransactionsIntegration) SetupOnce(client *Client) {
iei.ignoreTransactions = transformStringsIntoRegexps(client.options.IgnoreTransactions)
client.AddEventProcessor(iei.processor)
}
func (iei *ignoreTransactionsIntegration) processor(event *Event, _ *EventHint) *Event {
suspect := event.Transaction
if suspect == "" {
return event
}
for _, pattern := range iei.ignoreTransactions {
if pattern.Match([]byte(suspect)) {
Logger.Printf("Transaction dropped due to being matched by `IgnoreTransactions` option."+
"| Value matched: %s | Filter used: %s", suspect, pattern)
return nil
}
}
return event
}
// ================================
// Contextify Frames Integration
// ================================
@@ -198,7 +233,7 @@ func (cfi *contextifyFramesIntegration) SetupOnce(client *Client) {
client.AddEventProcessor(cfi.processor)
}
func (cfi *contextifyFramesIntegration) processor(event *Event, hint *EventHint) *Event {
func (cfi *contextifyFramesIntegration) processor(event *Event, _ *EventHint) *Event {
// Range over all exceptions
for _, ex := range event.Exception {
// If it has no stacktrace, just bail out
@@ -291,3 +326,66 @@ func (cfi *contextifyFramesIntegration) addContextLinesToFrame(frame Frame, line
}
return frame
}
// ================================
// Global Tags Integration
// ================================
const envTagsPrefix = "SENTRY_TAGS_"
type globalTagsIntegration struct {
tags map[string]string
envTags map[string]string
}
func (ti *globalTagsIntegration) Name() string {
return "GlobalTags"
}
func (ti *globalTagsIntegration) SetupOnce(client *Client) {
ti.tags = make(map[string]string, len(client.options.Tags))
for k, v := range client.options.Tags {
ti.tags[k] = v
}
ti.envTags = loadEnvTags()
client.AddEventProcessor(ti.processor)
}
func (ti *globalTagsIntegration) processor(event *Event, _ *EventHint) *Event {
if len(ti.tags) == 0 && len(ti.envTags) == 0 {
return event
}
if event.Tags == nil {
event.Tags = make(map[string]string, len(ti.tags)+len(ti.envTags))
}
for k, v := range ti.tags {
if _, ok := event.Tags[k]; !ok {
event.Tags[k] = v
}
}
for k, v := range ti.envTags {
if _, ok := event.Tags[k]; !ok {
event.Tags[k] = v
}
}
return event
}
func loadEnvTags() map[string]string {
tags := map[string]string{}
for _, pair := range os.Environ() {
parts := strings.Split(pair, "=")
if !strings.HasPrefix(parts[0], envTagsPrefix) {
continue
}
tag := strings.TrimPrefix(parts[0], envTagsPrefix)
tags[tag] = parts[1]
}
return tags
}

View File

@@ -11,15 +11,14 @@ import (
"time"
)
// Protocol Docs (kinda)
// https://github.com/getsentry/rust-sentry-types/blob/master/src/protocol/v7.rs
// eventType is the type of an error event.
const eventType = "event"
// transactionType is the type of a transaction event.
const transactionType = "transaction"
// eventType is the type of an error event.
const eventType = "event"
// profileType is the type of a profile event.
// currently, profiles are always sent as part of a transaction event.
const profileType = "profile"
// checkInType is the type of a check in event.
@@ -242,7 +241,7 @@ type Exception struct {
Type string `json:"type,omitempty"` // used as the main issue title
Value string `json:"value,omitempty"` // used as the main issue subtitle
Module string `json:"module,omitempty"`
ThreadID string `json:"thread_id,omitempty"`
ThreadID uint64 `json:"thread_id,omitempty"`
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
Mechanism *Mechanism `json:"mechanism,omitempty"`
}
@@ -318,6 +317,7 @@ type Event struct {
Request *Request `json:"request,omitempty"`
Exception []Exception `json:"exception,omitempty"`
DebugMeta *DebugMeta `json:"debug_meta,omitempty"`
Attachments []*Attachment `json:"-"`
// The fields below are only relevant for transactions.
@@ -334,7 +334,6 @@ type Event struct {
// The fields below are not part of the final JSON payload.
sdkMetaData SDKMetaData
attachments []*Attachment
}
// SetException appends the unwrapped errors to the event's exception list.

View File

@@ -236,18 +236,32 @@ func (p *profileRecorder) getBuckets(relativeStartNS, relativeEndNS uint64) (sam
return 0, nil, nil
}
// Search for the first item after the given startTime.
var start = end
samplesCount = 0
buckets = make([]*profileSamplesBucket, 0, int64((relativeEndNS-relativeStartNS)/uint64(profilerSamplingRate.Nanoseconds()))+1)
for start.Value != nil {
var bucket = start.Value.(*profileSamplesBucket)
if bucket.relativeTimeNS < relativeStartNS {
break
{ // Find the first item after the given startTime.
var start = end
var prevBucket *profileSamplesBucket
samplesCount = 0
buckets = make([]*profileSamplesBucket, 0, int64((relativeEndNS-relativeStartNS)/uint64(profilerSamplingRate.Nanoseconds()))+1)
for start.Value != nil {
var bucket = start.Value.(*profileSamplesBucket)
// If this bucket's time is before the requests start time, don't collect it (and stop iterating further).
if bucket.relativeTimeNS < relativeStartNS {
break
}
// If this bucket time is greater than previous the bucket's time, we have exhausted the whole ring buffer
// before we were able to find the start time. That means the start time is not present and we must break.
// This happens if the slice duration exceeds the ring buffer capacity.
if prevBucket != nil && bucket.relativeTimeNS > prevBucket.relativeTimeNS {
break
}
samplesCount += len(bucket.goIDs)
buckets = append(buckets, bucket)
start = start.Prev()
prevBucket = bucket
}
samplesCount += len(bucket.goIDs)
buckets = append(buckets, bucket)
start = start.Prev()
}
// Edge case - if the period requested was too short and we haven't collected enough samples.

View File

@@ -344,7 +344,7 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
}
if len(scope.attachments) > 0 {
event.attachments = append(event.attachments, scope.attachments...)
event.Attachments = append(event.Attachments, scope.attachments...)
}
if len(scope.tags) > 0 {

View File

@@ -6,15 +6,7 @@ import (
)
// The version of the SDK.
// Deprecated: To be removed in 0.25.0. Use SDKVersion instead.
const Version = SDKVersion
// The version of the SDK.
const SDKVersion = "0.24.0"
// The identifier of the SDK.
// Deprecated: To be removed in 0.25.0. Use Client.GetSDKIdentifier() instead.
const SDKIdentifier = "sentry.go"
const SDKVersion = "0.27.0"
// apiVersion is the minimum version of the Sentry API compatible with the
// sentry-go SDK.

View File

@@ -355,9 +355,7 @@ func callerFunctionName() string {
// It replicates https://golang.org/pkg/debug/gosym/#Sym.PackageName, avoiding a
// dependency on debug/gosym.
func packageName(name string) string {
// A prefix of "type." and "go." is a compiler-generated symbol that doesn't belong to any package.
// See variable reservedimports in cmd/compile/internal/gc/subr.go
if strings.HasPrefix(name, "go.") || strings.HasPrefix(name, "type.") {
if isCompilerGeneratedSymbol(name) {
return ""
}

View File

@@ -0,0 +1,15 @@
//go:build !go1.20
package sentry
import "strings"
func isCompilerGeneratedSymbol(name string) bool {
// In versions of Go below 1.20 a prefix of "type." and "go." is a
// compiler-generated symbol that doesn't belong to any package.
// See variable reservedimports in cmd/compile/internal/gc/subr.go
if strings.HasPrefix(name, "go.") || strings.HasPrefix(name, "type.") {
return true
}
return false
}

View File

@@ -0,0 +1,15 @@
//go:build go1.20
package sentry
import "strings"
func isCompilerGeneratedSymbol(name string) bool {
// In versions of Go 1.20 and above a prefix of "type:" and "go:" is a
// compiler-generated symbol that doesn't belong to any package.
// See variable reservedimports in cmd/compile/internal/gc/subr.go
if strings.HasPrefix(name, "go:") || strings.HasPrefix(name, "type:") {
return true
}
return false
}

View File

@@ -49,11 +49,6 @@ type Span struct { //nolint: maligned // prefer readability over optimal memory
// parent refers to the immediate local parent span. A remote parent span is
// only referenced by setting ParentSpanID.
parent *Span
// isTransaction is true only for the root span of a local span tree. The
// root span is the first span started in a context. Note that a local root
// span may have a remote parent belonging to the same trace, therefore
// isTransaction depends on ctx and not on parent.
isTransaction bool
// recorder stores all spans in a transaction. Guaranteed to be non-nil.
recorder *spanRecorder
// span context, can only be set on transactions
@@ -114,9 +109,8 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp
StartTime: time.Now(),
Sampled: SampledUndefined,
ctx: context.WithValue(ctx, spanContextKey{}, &span),
parent: parent,
isTransaction: !hasParent,
ctx: context.WithValue(ctx, spanContextKey{}, &span),
parent: parent,
}
if hasParent {
@@ -257,7 +251,7 @@ func (s *Span) SetContext(key string, value Context) {
// IsTransaction checks if the given span is a transaction.
func (s *Span) IsTransaction() bool {
return s.isTransaction
return s.parent == nil
}
// GetTransaction returns the transaction that contains this span.
@@ -289,7 +283,7 @@ func (s *Span) GetTransaction() *Span {
// func (s *Span) TransactionName() string
// func (s *Span) SetTransactionName(name string)
// ToSentryTrace returns the seralized TraceParentContext from a transaction/sapn.
// ToSentryTrace returns the seralized TraceParentContext from a transaction/span.
// Use this function to propagate the TraceParentContext to a downstream SDK,
// either as the value of the "sentry-trace" HTTP header, or as an html "sentry-trace" meta tag.
func (s *Span) ToSentryTrace() string {
@@ -326,7 +320,7 @@ func (s *Span) ToBaggage() string {
// SetDynamicSamplingContext sets the given dynamic sampling context on the
// current transaction.
func (s *Span) SetDynamicSamplingContext(dsc DynamicSamplingContext) {
if s.isTransaction {
if s.IsTransaction() {
s.dynamicSamplingContext = dsc
}
}
@@ -391,7 +385,7 @@ func (s *Span) updateFromSentryTrace(header []byte) (updated bool) {
}
func (s *Span) updateFromBaggage(header []byte) {
if s.isTransaction {
if s.IsTransaction() {
dsc, err := DynamicSamplingContextFromHeader(header)
if err != nil {
return
@@ -452,7 +446,7 @@ func (s *Span) sample() Sampled {
// Note: non-transaction should always have a parent, but we check both
// conditions anyway -- the first for semantic meaning, the second to
// avoid a nil pointer dereference.
if !s.isTransaction && s.parent != nil {
if !s.IsTransaction() && s.parent != nil {
return s.parent.Sampled
}
@@ -516,7 +510,7 @@ func (s *Span) toEvent() *Event {
s.mu.Lock()
defer s.mu.Unlock()
if !s.isTransaction {
if !s.IsTransaction() {
return nil // only transactions can be transformed into events
}
@@ -829,17 +823,6 @@ func (s Sampled) Bool() bool {
// A SpanOption is a function that can modify the properties of a span.
type SpanOption func(s *Span)
// The TransactionName option sets the name of the current transaction.
//
// A span tree has a single transaction name, therefore using this option when
// starting a span affects the span tree as a whole, potentially overwriting a
// name set previously.
//
// Deprecated: Use WithTransactionName() instead.
func TransactionName(name string) SpanOption {
return WithTransactionName(name)
}
// WithTransactionName option sets the name of the current transaction.
//
// A span tree has a single transaction name, therefore using this option when
@@ -851,11 +834,11 @@ func WithTransactionName(name string) SpanOption {
}
}
// OpName sets the operation name for a given span.
//
// Deprecated: Use WithOpName() instead.
func OpName(name string) SpanOption {
return WithOpName(name)
// WithDescription sets the description of a span.
func WithDescription(description string) SpanOption {
return func(s *Span) {
s.Description = description
}
}
// WithOpName sets the operation name for a given span.
@@ -865,13 +848,6 @@ func WithOpName(name string) SpanOption {
}
}
// TransctionSource sets the source of the transaction name.
//
// Deprecated: Use WithTransactionSource() instead.
func TransctionSource(source TransactionSource) SpanOption {
return WithTransactionSource(source)
}
// WithTransactionSource sets the source of the transaction name.
//
// Note: if the transaction source is not a valid source (as described
@@ -883,13 +859,6 @@ func WithTransactionSource(source TransactionSource) SpanOption {
}
}
// SpanSampled updates the sampling flag for a given span.
//
// Deprecated: Use WithSpanSampled() instead.
func SpanSampled(sampled Sampled) SpanOption {
return WithSpanSampled(sampled)
}
// WithSpanSampled updates the sampling flag for a given span.
func WithSpanSampled(sampled Sampled) SpanOption {
return func(s *Span) {

View File

@@ -185,7 +185,7 @@ func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMes
}
// Attachments
for _, attachment := range event.attachments {
for _, attachment := range event.Attachments {
if err := encodeAttachment(enc, &b, attachment); err != nil {
return nil, err
}

View File

@@ -78,24 +78,28 @@ func defaultRelease() (release string) {
// v1.0.1-0-g9de4
// v2.0-8-g77df-dirty
// 4f72d7
cmd := exec.Command("git", "describe", "--long", "--always", "--dirty")
b, err := cmd.Output()
if err != nil {
// Either Git is not available or the current directory is not a
// Git repository.
var s strings.Builder
fmt.Fprintf(&s, "Release detection failed: %v", err)
if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 {
fmt.Fprintf(&s, ": %s", err.Stderr)
if _, err := exec.LookPath("git"); err == nil {
cmd := exec.Command("git", "describe", "--long", "--always", "--dirty")
b, err := cmd.Output()
if err != nil {
// Either Git is not available or the current directory is not a
// Git repository.
var s strings.Builder
fmt.Fprintf(&s, "Release detection failed: %v", err)
if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 {
fmt.Fprintf(&s, ": %s", err.Stderr)
}
Logger.Print(s.String())
} else {
release = strings.TrimSpace(string(b))
Logger.Printf("Using release from Git: %s", release)
return release
}
Logger.Print(s.String())
Logger.Print("Some Sentry features will not be available. See https://docs.sentry.io/product/releases/.")
Logger.Print("To stop seeing this message, pass a Release to sentry.Init or set the SENTRY_RELEASE environment variable.")
return ""
}
release = strings.TrimSpace(string(b))
Logger.Printf("Using release from Git: %s", release)
return release
Logger.Print("Some Sentry features will not be available. See https://docs.sentry.io/product/releases/.")
Logger.Print("To stop seeing this message, pass a Release to sentry.Init or set the SENTRY_RELEASE environment variable.")
return ""
}
func revisionFromBuildInfo(info *debug.BuildInfo) string {