sourcegraph/internal/database/event_logs.go
Eric Fritz e2ff2b1f3f
symbols: Bring this baby into 2021 (#27986)
Co-authored-by: Noah Santschi-Cooney <noah@santschi-cooney.ch>
2021-11-30 17:56:45 +00:00

1278 lines
45 KiB
Go

package database
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
"github.com/cockroachdb/errors"
"github.com/keegancsmith/sqlf"
"github.com/lib/pq"
"github.com/sourcegraph/sourcegraph/internal/conf"
"github.com/sourcegraph/sourcegraph/internal/database/basestore"
"github.com/sourcegraph/sourcegraph/internal/database/batch"
"github.com/sourcegraph/sourcegraph/internal/database/dbutil"
"github.com/sourcegraph/sourcegraph/internal/featureflag"
"github.com/sourcegraph/sourcegraph/internal/timeutil"
"github.com/sourcegraph/sourcegraph/internal/types"
"github.com/sourcegraph/sourcegraph/internal/version"
)
const (
integrationSource = "CODEHOSTINTEGRATION"
)
type EventLogStore interface {
// AggregatedCodeIntelEvents calculates CodeIntelAggregatedEvent for each every unique event type related to code intel.
AggregatedCodeIntelEvents(ctx context.Context) ([]types.CodeIntelAggregatedEvent, error)
// AggregatedSearchEvents calculates SearchAggregatedEvent for each every unique event type related to search.
AggregatedSearchEvents(ctx context.Context, now time.Time) ([]types.SearchAggregatedEvent, error)
BulkInsert(ctx context.Context, events []*Event) error
// CodeIntelligenceCrossRepositoryWAUs returns the WAU (current week) with any (precise or search-based) cross-repository code intelligence event.
CodeIntelligenceCrossRepositoryWAUs(ctx context.Context) (int, error)
// CodeIntelligencePreciseCrossRepositoryWAUs returns the WAU (current week) with precise-based cross-repository code intelligence events.
CodeIntelligencePreciseCrossRepositoryWAUs(ctx context.Context) (int, error)
// CodeIntelligencePreciseWAUs returns the WAU (current week) with precise-based code intelligence events.
CodeIntelligencePreciseWAUs(ctx context.Context) (int, error)
// CodeIntelligenceRepositoryCounts returns the counts of repositories with code intelligence
// properties (number of repositories with intel, with automatic/manual index configuration, etc).
CodeIntelligenceRepositoryCounts(ctx context.Context) (counts CodeIntelligenceRepositoryCounts, err error)
// CodeIntelligenceRepositoryCountsByLanguage returns the counts of repositories with code intelligence
// properties (number of repositories with intel, with automatic/manual index configuration, etc), grouped
// by language.
CodeIntelligenceRepositoryCountsByLanguage(ctx context.Context) (_ map[string]CodeIntelligenceRepositoryCountsForLanguage, err error)
// CodeIntelligenceSearchBasedCrossRepositoryWAUs returns the WAU (current week) with searched-base cross-repository code intelligence events.
CodeIntelligenceSearchBasedCrossRepositoryWAUs(ctx context.Context) (int, error)
// CodeIntelligenceSearchBasedWAUs returns the WAU (current week) with searched-base code intelligence events.
CodeIntelligenceSearchBasedWAUs(ctx context.Context) (int, error)
// CodeIntelligenceSettingsPageViewCount returns the number of view of pages related code intelligence
// administration (upload, index records, index configuration, etc) in the past week.
CodeIntelligenceSettingsPageViewCount(ctx context.Context) (int, error)
// CodeIntelligenceWAUs returns the WAU (current week) with any (precise or search-based) code intelligence event.
CodeIntelligenceWAUs(ctx context.Context) (int, error)
// CountByUserID gets a count of events logged by a given user.
CountByUserID(ctx context.Context, userID int32) (int, error)
// CountByUserIDAndEventName gets a count of events logged by a given user and with a given event name.
CountByUserIDAndEventName(ctx context.Context, userID int32, name string) (int, error)
// CountByUserIDAndEventNamePrefix gets a count of events logged by a given user and with a given event name prefix.
CountByUserIDAndEventNamePrefix(ctx context.Context, userID int32, namePrefix string) (int, error)
// CountByUserIDAndEventNames gets a count of events logged by a given user that match a list of given event names.
CountByUserIDAndEventNames(ctx context.Context, userID int32, names []string) (int, error)
// CountUniqueUsersAll provides a count of unique active users in a given time span.
CountUniqueUsersAll(ctx context.Context, startDate, endDate time.Time) (int, error)
// CountUniqueUsersByEventName provides a count of unique active users in a given time span that logged a given event.
CountUniqueUsersByEventName(ctx context.Context, startDate, endDate time.Time, name string) (int, error)
// CountUniqueUsersByEventNamePrefix provides a count of unique active users in a given time span that logged an event with a given prefix.
CountUniqueUsersByEventNamePrefix(ctx context.Context, startDate, endDate time.Time, namePrefix string) (int, error)
// CountUniqueUsersByEventNames provides a count of unique active users in a given time span that logged any event that matches a list of given event names
CountUniqueUsersByEventNames(ctx context.Context, startDate, endDate time.Time, names []string) (int, error)
// CountUniqueUsersPerPeriod provides a count of unique active users in a given time span, broken up into periods of
// a given type. The value of `now` should be the current time in UTC. Returns an array of length `periods`, with one
// entry for each period in the time span.
CountUniqueUsersPerPeriod(ctx context.Context, periodType PeriodType, now time.Time, periods int, opt *CountUniqueUsersOptions) ([]UsageValue, error)
Insert(ctx context.Context, e *Event) error
// LatestPing returns the most recently recorded ping event.
LatestPing(ctx context.Context) (*types.Event, error)
// ListAll gets all event logs in descending order of timestamp.
ListAll(ctx context.Context, opt EventLogsListOptions) ([]*types.Event, error)
ListUniqueUsersAll(ctx context.Context, startDate, endDate time.Time) ([]int32, error)
// MaxTimestampByUserID gets the max timestamp among event logs for a given user.
MaxTimestampByUserID(ctx context.Context, userID int32) (*time.Time, error)
// MaxTimestampByUserIDAndSource gets the max timestamp among event logs for a given user and event source.
MaxTimestampByUserIDAndSource(ctx context.Context, userID int32, source string) (*time.Time, error)
SiteUsage(ctx context.Context) (types.SiteUsageSummary, error)
// UsersUsageCounts returns a list of UserUsageCounts for all active users that produced 'SearchResultsQueried' and any
// '%codeintel%' events in the event_logs table.
UsersUsageCounts(ctx context.Context) (counts []types.UserUsageCounts, err error)
Transact(ctx context.Context) (EventLogStore, error)
Done(error) error
With(other basestore.ShareableStore) EventLogStore
basestore.ShareableStore
}
type eventLogStore struct {
*basestore.Store
}
// EventLogs instantiates and returns a new EventLogStore with prepared statements.
func EventLogs(db dbutil.DB) EventLogStore {
return &eventLogStore{Store: basestore.NewWithDB(db, sql.TxOptions{})}
}
// NewEventLogStoreWithDB instantiates and returns a new EventLogStore using the other store handle.
func EventLogsWith(other basestore.ShareableStore) EventLogStore {
return &eventLogStore{Store: basestore.NewWithHandle(other.Handle())}
}
func (l *eventLogStore) With(other basestore.ShareableStore) EventLogStore {
return &eventLogStore{Store: l.Store.With(other)}
}
func (l *eventLogStore) Transact(ctx context.Context) (EventLogStore, error) {
txBase, err := l.Store.Transact(ctx)
return &eventLogStore{Store: txBase}, err
}
// SanitizeEventURL makes the given URL is using HTTP/HTTPS scheme and within
// the current site determined by `conf.ExternalURL()`.
func SanitizeEventURL(raw string) string {
if raw == "" {
return ""
}
// Check if the URL looks like a real URL
u, err := url.Parse(raw)
if err != nil ||
(u.Scheme != "http" && u.Scheme != "https") {
return ""
}
// Check if the URL belongs to the current site
normalized := u.String()
if !strings.HasPrefix(normalized, conf.ExternalURL()) {
return ""
}
return normalized
}
// Event contains information needed for logging an event.
type Event struct {
Name string
URL string
UserID uint32
AnonymousUserID string
Argument json.RawMessage
PublicArgument json.RawMessage
Source string
Timestamp time.Time
FeatureFlags featureflag.FlagSet
CohortID *string // date in YYYY-MM-DD format
}
func (l *eventLogStore) Insert(ctx context.Context, e *Event) error {
return l.BulkInsert(ctx, []*Event{e})
}
func (l *eventLogStore) BulkInsert(ctx context.Context, events []*Event) error {
coalesce := func(v json.RawMessage) json.RawMessage {
if v != nil {
return v
}
return json.RawMessage(`{}`)
}
rowValues := make(chan []interface{}, len(events))
for _, event := range events {
featureFlags, err := json.Marshal(event.FeatureFlags)
if err != nil {
return err
}
rowValues <- []interface{}{
event.Name,
// 🚨 SECURITY: It is important to sanitize event URL before
// being stored to the database to help guarantee no malicious
// data at rest.
SanitizeEventURL(event.URL),
event.UserID,
event.AnonymousUserID,
event.Source,
coalesce(event.Argument),
coalesce(event.PublicArgument),
version.Version(),
event.Timestamp.UTC(),
featureFlags,
event.CohortID,
}
}
close(rowValues)
return batch.InsertValues(
ctx,
l.Handle().DB(),
"event_logs",
batch.MaxNumPostgresParameters,
[]string{
"name",
"url",
"user_id",
"anonymous_user_id",
"source",
"argument",
"public_argument",
"version",
"timestamp",
"feature_flags",
"cohort_id",
},
rowValues,
)
}
func (l *eventLogStore) getBySQL(ctx context.Context, querySuffix *sqlf.Query) ([]*types.Event, error) {
q := sqlf.Sprintf("SELECT id, name, url, user_id, anonymous_user_id, source, argument, version, timestamp FROM event_logs %s", querySuffix)
rows, err := l.Query(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
events := []*types.Event{}
for rows.Next() {
r := types.Event{}
err := rows.Scan(&r.ID, &r.Name, &r.URL, &r.UserID, &r.AnonymousUserID, &r.Source, &r.Argument, &r.Version, &r.Timestamp)
if err != nil {
return nil, err
}
events = append(events, &r)
}
if err := rows.Err(); err != nil {
return nil, err
}
return events, nil
}
// EventLogsListOptions specifies the options for listing event logs.
type EventLogsListOptions struct {
// UserID specifies the user whose events should be included.
UserID int32
*LimitOffset
EventName *string
}
func (l *eventLogStore) ListAll(ctx context.Context, opt EventLogsListOptions) ([]*types.Event, error) {
conds := []*sqlf.Query{sqlf.Sprintf("TRUE")}
if opt.UserID != 0 {
conds = append(conds, sqlf.Sprintf("user_id = %d", opt.UserID))
}
if opt.EventName != nil {
conds = append(conds, sqlf.Sprintf("name = %s", opt.EventName))
}
return l.getBySQL(ctx, sqlf.Sprintf("WHERE %s ORDER BY timestamp DESC %s", sqlf.Join(conds, "AND"), opt.LimitOffset.SQL()))
}
func (l *eventLogStore) LatestPing(ctx context.Context) (*types.Event, error) {
rows, err := l.getBySQL(ctx, sqlf.Sprintf(`WHERE name='ping' ORDER BY id DESC LIMIT 1`))
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, sql.ErrNoRows
}
return rows[0], err
}
func (l *eventLogStore) CountByUserID(ctx context.Context, userID int32) (int, error) {
return l.countBySQL(ctx, sqlf.Sprintf("WHERE user_id = %d", userID))
}
func (l *eventLogStore) CountByUserIDAndEventName(ctx context.Context, userID int32, name string) (int, error) {
return l.countBySQL(ctx, sqlf.Sprintf("WHERE user_id = %d AND name = %s", userID, name))
}
func (l *eventLogStore) CountByUserIDAndEventNamePrefix(ctx context.Context, userID int32, namePrefix string) (int, error) {
return l.countBySQL(ctx, sqlf.Sprintf("WHERE user_id = %d AND name LIKE %s", userID, namePrefix+"%"))
}
func (l *eventLogStore) CountByUserIDAndEventNames(ctx context.Context, userID int32, names []string) (int, error) {
items := []*sqlf.Query{}
for _, v := range names {
items = append(items, sqlf.Sprintf("%s", v))
}
return l.countBySQL(ctx, sqlf.Sprintf("WHERE user_id = %d AND name IN (%s)", userID, sqlf.Join(items, ",")))
}
// countBySQL gets a count of event logs.
func (l *eventLogStore) countBySQL(ctx context.Context, querySuffix *sqlf.Query) (int, error) {
q := sqlf.Sprintf("SELECT COUNT(*) FROM event_logs %s", querySuffix)
r := l.QueryRow(ctx, q)
var count int
err := r.Scan(&count)
return count, err
}
func (l *eventLogStore) MaxTimestampByUserID(ctx context.Context, userID int32) (*time.Time, error) {
return l.maxTimestampBySQL(ctx, sqlf.Sprintf("WHERE user_id = %d", userID))
}
func (l *eventLogStore) MaxTimestampByUserIDAndSource(ctx context.Context, userID int32, source string) (*time.Time, error) {
return l.maxTimestampBySQL(ctx, sqlf.Sprintf("WHERE user_id = %d AND source = %s", userID, source))
}
// maxTimestampBySQL gets the max timestamp among event logs.
func (l *eventLogStore) maxTimestampBySQL(ctx context.Context, querySuffix *sqlf.Query) (*time.Time, error) {
q := sqlf.Sprintf("SELECT MAX(timestamp) FROM event_logs %s", querySuffix)
r := l.QueryRow(ctx, q)
var t time.Time
err := r.Scan(&dbutil.NullTime{Time: &t})
if t.IsZero() {
return nil, err
}
return &t, err
}
// UsageValue is a single count of usage for a time period starting on a given date.
type UsageValue struct {
Start time.Time
Count int
}
// PeriodType is the type of period in which to count events and unique users.
type PeriodType string
const (
// Daily is used to get a count of events or unique users within a day.
Daily PeriodType = "daily"
// Weekly is used to get a count of events or unique users within a week.
Weekly PeriodType = "weekly"
// Monthly is used to get a count of events or unique users within a month.
Monthly PeriodType = "monthly"
)
// intervalByPeriodType is a map of generate_series step values by period type.
var intervalByPeriodType = map[PeriodType]*sqlf.Query{
Daily: sqlf.Sprintf("'1 day'"),
Weekly: sqlf.Sprintf("'1 week'"),
Monthly: sqlf.Sprintf("'1 month'"),
}
// periodByPeriodType is a map of SQL fragments that produce a timestamp bucket by period
// type. This assumes the existence of a field named `timestamp` in the enclosing query.
var periodByPeriodType = map[PeriodType]*sqlf.Query{
Daily: sqlf.Sprintf(makeDateTruncExpression("day", "timestamp")),
Weekly: sqlf.Sprintf(makeDateTruncExpression("week", "timestamp")),
Monthly: sqlf.Sprintf(makeDateTruncExpression("month", "timestamp")),
}
// calcStartDate calculates the the starting date of a number of periods given the period type.
// from the current time supplied as `now`. Returns a second false value if the period type is
// illegal.
func calcStartDate(now time.Time, periodType PeriodType, periods int) (time.Time, bool) {
periodsAgo := periods - 1
switch periodType {
case Daily:
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, -periodsAgo), true
case Weekly:
return timeutil.StartOfWeek(now, periodsAgo), true
case Monthly:
return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC).AddDate(0, -periodsAgo, 0), true
}
return time.Time{}, false
}
// calcEndDate calculates the the ending date of a number of periods given the period type.
// Returns a second false value if the period type is illegal.
func calcEndDate(startDate time.Time, periodType PeriodType, periods int) (time.Time, bool) {
periodsAgo := periods - 1
switch periodType {
case Daily:
return startDate.AddDate(0, 0, periodsAgo), true
case Weekly:
return startDate.AddDate(0, 0, 7*periodsAgo), true
case Monthly:
return startDate.AddDate(0, periodsAgo, 0), true
}
return time.Time{}, false
}
// CountUniqueUsersOptions provides options for counting unique users.
type CountUniqueUsersOptions struct {
// If true, only include registered users. Otherwise, include all users.
RegisteredOnly bool
// If true, only include code host integration users. Otherwise, include all users.
IntegrationOnly bool
// If set, adds additional restrictions on the event types.
EventFilters *EventFilterOptions
}
// EventFilterOptions provides options for filtering events.
type EventFilterOptions struct {
// If set, only include events with a given prefix.
ByEventNamePrefix string
// If set, only include events with the given name.
ByEventName string
// If not empty, only include events that matche a list of given event names
ByEventNames []string
// Must be used with ByEventName
//
// If set, only include events that match a specified condition.
ByEventNameWithCondition *sqlf.Query
}
// EventArgumentMatch provides the options for matching an event with
// a specific JSON value passed as an argument.
type EventArgumentMatch struct {
// The name of the JSON key to match against.
ArgumentName string
// The actual value passed to the JSON key to match.
ArgumentValue string
}
// PercentileValue is a slice of Nth percentile values calculated from a field of events
// in a time period starting on a given date.
type PercentileValue struct {
Start time.Time
Values []float64
}
func (l *eventLogStore) CountUniqueUsersPerPeriod(ctx context.Context, periodType PeriodType, now time.Time, periods int, opt *CountUniqueUsersOptions) ([]UsageValue, error) {
startDate, ok := calcStartDate(now, periodType, periods)
if !ok {
return nil, errors.Errorf("periodType must be \"daily\", \"weekly\", or \"monthly\". Got %s", periodType)
}
endDate, ok := calcEndDate(startDate, periodType, periods)
if !ok {
return nil, errors.Errorf("periodType must be \"daily\", \"weekly\", or \"monthly\". Got %s", periodType)
}
conds := []*sqlf.Query{sqlf.Sprintf("TRUE")}
if opt != nil {
if opt.RegisteredOnly {
conds = append(conds, sqlf.Sprintf("user_id > 0"))
}
if opt.IntegrationOnly {
conds = append(conds, sqlf.Sprintf("source = %s", integrationSource))
}
if opt.EventFilters != nil {
if opt.EventFilters.ByEventNamePrefix != "" {
conds = append(conds, sqlf.Sprintf("name LIKE %s", opt.EventFilters.ByEventNamePrefix+"%"))
}
if opt.EventFilters.ByEventName != "" {
conds = append(conds, sqlf.Sprintf("name = %s", opt.EventFilters.ByEventName))
}
if opt.EventFilters.ByEventNameWithCondition != nil {
conds = append(conds, opt.EventFilters.ByEventNameWithCondition)
}
if len(opt.EventFilters.ByEventNames) > 0 {
items := []*sqlf.Query{}
for _, v := range opt.EventFilters.ByEventNames {
items = append(items, sqlf.Sprintf("%s", v))
}
conds = append(conds, sqlf.Sprintf("name IN (%s)", sqlf.Join(items, ",")))
}
}
}
return l.countUniqueUsersPerPeriodBySQL(ctx, intervalByPeriodType[periodType], periodByPeriodType[periodType], startDate, endDate, conds)
}
func (l *eventLogStore) countUniqueUsersPerPeriodBySQL(ctx context.Context, interval, period *sqlf.Query, startDate, endDate time.Time, conds []*sqlf.Query) ([]UsageValue, error) {
return l.countPerPeriodBySQL(ctx, sqlf.Sprintf("DISTINCT "+userIDQueryFragment), interval, period, startDate, endDate, conds)
}
func (l *eventLogStore) countPerPeriodBySQL(ctx context.Context, countExpr, interval, period *sqlf.Query, startDate, endDate time.Time, conds []*sqlf.Query) ([]UsageValue, error) {
allPeriods := sqlf.Sprintf("SELECT generate_series((%s)::timestamp, (%s)::timestamp, (%s)::interval) AS period", startDate, endDate, interval)
countByPeriod := sqlf.Sprintf(`SELECT (%s) AS period, COUNT(%s) AS count
FROM event_logs
WHERE (%s)
GROUP BY period`, period, countExpr, sqlf.Join(conds, ") AND ("))
q := sqlf.Sprintf(`WITH all_periods AS (%s), count_by_period AS (%s)
SELECT all_periods.period, COALESCE(count, 0)
FROM all_periods
LEFT OUTER JOIN count_by_period ON all_periods.period = (count_by_period.period)::timestamp
ORDER BY period DESC`, allPeriods, countByPeriod)
rows, err := l.Query(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
counts := []UsageValue{}
for rows.Next() {
var v UsageValue
err := rows.Scan(&v.Start, &v.Count)
if err != nil {
return nil, err
}
v.Start = v.Start.UTC()
counts = append(counts, v)
}
if err = rows.Err(); err != nil {
return nil, err
}
return counts, nil
}
func (l *eventLogStore) CountUniqueUsersAll(ctx context.Context, startDate, endDate time.Time) (int, error) {
return l.countUniqueUsersBySQL(ctx, startDate, endDate, nil)
}
func (l *eventLogStore) CountUniqueUsersByEventNamePrefix(ctx context.Context, startDate, endDate time.Time, namePrefix string) (int, error) {
return l.countUniqueUsersBySQL(ctx, startDate, endDate, sqlf.Sprintf("AND name LIKE %s ", namePrefix+"%"))
}
func (l *eventLogStore) CountUniqueUsersByEventName(ctx context.Context, startDate, endDate time.Time, name string) (int, error) {
return l.countUniqueUsersBySQL(ctx, startDate, endDate, sqlf.Sprintf("AND name = %s", name))
}
func (l *eventLogStore) CountUniqueUsersByEventNames(ctx context.Context, startDate, endDate time.Time, names []string) (int, error) {
items := []*sqlf.Query{}
for _, v := range names {
items = append(items, sqlf.Sprintf("%s", v))
}
return l.countUniqueUsersBySQL(ctx, startDate, endDate, sqlf.Sprintf("AND name IN (%s)", sqlf.Join(items, ",")))
}
func (l *eventLogStore) countUniqueUsersBySQL(ctx context.Context, startDate, endDate time.Time, querySuffix *sqlf.Query) (int, error) {
if querySuffix == nil {
querySuffix = sqlf.Sprintf("")
}
q := sqlf.Sprintf(`SELECT COUNT(DISTINCT `+userIDQueryFragment+`)
FROM event_logs
WHERE (DATE(TIMEZONE('UTC'::text, timestamp)) >= %s) AND (DATE(TIMEZONE('UTC'::text, timestamp)) <= %s) %s`, startDate, endDate, querySuffix)
r := l.QueryRow(ctx, q)
var count int
err := r.Scan(&count)
return count, err
}
func (l *eventLogStore) ListUniqueUsersAll(ctx context.Context, startDate, endDate time.Time) ([]int32, error) {
rows, err := l.Handle().DB().QueryContext(ctx, `SELECT user_id
FROM event_logs
WHERE user_id > 0 AND DATE(TIMEZONE('UTC'::text, timestamp)) >= $1 AND DATE(TIMEZONE('UTC'::text, timestamp)) <= $2
GROUP BY user_id`, startDate, endDate)
if err != nil {
return nil, err
}
var users []int32
defer rows.Close()
for rows.Next() {
var userID int32
err := rows.Scan(&userID)
if err != nil {
return nil, err
}
users = append(users, userID)
}
if err = rows.Err(); err != nil {
return nil, err
}
return users, nil
}
func (l *eventLogStore) UsersUsageCounts(ctx context.Context) (counts []types.UserUsageCounts, err error) {
rows, err := l.Handle().DB().QueryContext(ctx, usersUsageCountsQuery)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var c types.UserUsageCounts
err := rows.Scan(
&c.Date,
&c.UserID,
&dbutil.NullInt32{N: &c.SearchCount},
&dbutil.NullInt32{N: &c.CodeIntelCount},
)
if err != nil {
return nil, err
}
counts = append(counts, c)
}
if err = rows.Err(); err != nil {
return nil, err
}
return counts, nil
}
const usersUsageCountsQuery = `
-- source: internal/database/event_logs.go:UsersUsageCounts
SELECT
DATE(timestamp),
user_id,
COUNT(*) FILTER (WHERE event_logs.name ='SearchResultsQueried') as search_count,
COUNT(*) FILTER (WHERE event_logs.name LIKE '%codeintel%') as codeintel_count
FROM event_logs
GROUP BY 1, 2
ORDER BY 1 DESC, 2 ASC;
`
func (l *eventLogStore) SiteUsage(ctx context.Context) (types.SiteUsageSummary, error) {
return l.siteUsage(ctx, time.Now().UTC())
}
func (l *eventLogStore) siteUsage(ctx context.Context, now time.Time) (summary types.SiteUsageSummary, err error) {
query := sqlf.Sprintf(siteUsageQuery, now, now, now, now)
err = l.QueryRow(ctx, query).Scan(
&summary.Month,
&summary.Week,
&summary.Day,
&summary.UniquesMonth,
&summary.UniquesWeek,
&summary.UniquesDay,
&summary.RegisteredUniquesMonth,
&summary.RegisteredUniquesWeek,
&summary.RegisteredUniquesDay,
&summary.IntegrationUniquesMonth,
&summary.IntegrationUniquesWeek,
&summary.IntegrationUniquesDay,
&summary.ManageUniquesMonth,
&summary.CodeUniquesMonth,
&summary.VerifyUniquesMonth,
&summary.MonitorUniquesMonth,
&summary.ManageUniquesWeek,
&summary.CodeUniquesWeek,
&summary.VerifyUniquesWeek,
&summary.MonitorUniquesWeek,
)
return summary, err
}
var siteUsageQuery = `
SELECT
current_month,
current_week,
current_day,
COUNT(DISTINCT user_id) FILTER (WHERE month = current_month) AS uniques_month,
COUNT(DISTINCT user_id) FILTER (WHERE week = current_week) AS uniques_week,
COUNT(DISTINCT user_id) FILTER (WHERE day = current_day) AS uniques_day,
COUNT(DISTINCT user_id) FILTER (WHERE month = current_month AND registered) AS registered_uniques_month,
COUNT(DISTINCT user_id) FILTER (WHERE week = current_week AND registered) AS registered_uniques_week,
COUNT(DISTINCT user_id) FILTER (WHERE day = current_day AND registered) AS registered_uniques_day,
COUNT(DISTINCT user_id) FILTER (WHERE month = current_month AND source = 'CODEHOSTINTEGRATION')
AS integration_uniques_month,
COUNT(DISTINCT user_id) FILTER (WHERE week = current_week AND source = 'CODEHOSTINTEGRATION')
AS integration_uniques_week,
COUNT(DISTINCT user_id) FILTER (WHERE day = current_day AND source = 'CODEHOSTINTEGRATION')
AS integration_uniques_day,
COUNT(DISTINCT user_id) FILTER (
WHERE month = current_month AND name LIKE 'ViewSiteAdmin%%%%'
) AS manage_uniques_month,
COUNT(DISTINCT user_id) FILTER (
WHERE month = current_month AND name IN (
'ViewRepository',
'ViewBlob',
'ViewTree',
'SearchResultsQueried'
)
) AS code_uniques_month,
COUNT(DISTINCT user_id) FILTER (
WHERE month = current_month AND name IN (
'SavedSearchEmailClicked',
'SavedSearchSlackClicked',
'SavedSearchEmailNotificationSent'
)
) AS verify_uniques_month,
COUNT(DISTINCT user_id) FILTER (
WHERE month = current_month AND name IN (
'DiffSearchResultsQueried'
)
) AS monitor_uniques_month,
COUNT(DISTINCT user_id) FILTER (
WHERE week = current_week AND name LIKE 'ViewSiteAdmin%%%%'
) AS manage_uniques_week,
COUNT(DISTINCT user_id) FILTER (
WHERE week = current_week AND name IN (
'ViewRepository',
'ViewBlob',
'ViewTree',
'SearchResultsQueried'
)
) AS code_uniques_week,
COUNT(DISTINCT user_id) FILTER (
WHERE week = current_week AND name IN (
'SavedSearchEmailClicked',
'SavedSearchSlackClicked',
'SavedSearchEmailNotificationSent'
)
) AS verify_uniques_week,
COUNT(DISTINCT user_id) FILTER (
WHERE week = current_week AND name IN (
'DiffSearchResultsQueried'
)
) AS monitor_uniques_week
FROM (
-- This sub-query is here to avoid re-doing this work above on each aggregation.
SELECT
name,
user_id != 0 as registered,
` + aggregatedUserIDQueryFragment + ` AS user_id,
source,
` + makeDateTruncExpression("month", "timestamp") + ` as month,
` + makeDateTruncExpression("week", "timestamp") + ` as week,
` + makeDateTruncExpression("day", "timestamp") + ` as day,
` + makeDateTruncExpression("month", "%s::timestamp") + ` as current_month,
` + makeDateTruncExpression("week", "%s::timestamp") + ` as current_week,
` + makeDateTruncExpression("day", "%s::timestamp") + ` as current_day
FROM event_logs
WHERE timestamp >= ` + makeDateTruncExpression("month", "%s::timestamp") + `
) events
GROUP BY current_month, current_week, current_day
`
func (l *eventLogStore) CodeIntelligencePreciseWAUs(ctx context.Context) (int, error) {
eventNames := []string{
"codeintel.lsifHover",
"codeintel.lsifDefinitions",
"codeintel.lsifReferences",
}
return l.codeIntelligenceWeeklyUsersCount(ctx, eventNames, time.Now().UTC())
}
func (l *eventLogStore) CodeIntelligenceSearchBasedWAUs(ctx context.Context) (int, error) {
eventNames := []string{
"codeintel.searchHover",
"codeintel.searchDefinitions",
"codeintel.searchReferences",
}
return l.codeIntelligenceWeeklyUsersCount(ctx, eventNames, time.Now().UTC())
}
func (l *eventLogStore) CodeIntelligenceWAUs(ctx context.Context) (int, error) {
eventNames := []string{
"codeintel.lsifHover",
"codeintel.lsifDefinitions",
"codeintel.lsifReferences",
"codeintel.searchHover",
"codeintel.searchDefinitions",
"codeintel.searchReferences",
}
return l.codeIntelligenceWeeklyUsersCount(ctx, eventNames, time.Now().UTC())
}
func (l *eventLogStore) CodeIntelligenceCrossRepositoryWAUs(ctx context.Context) (int, error) {
eventNames := []string{
"codeintel.lsifDefinitions.xrepo",
"codeintel.lsifReferences.xrepo",
"codeintel.searchDefinitions.xrepo",
"codeintel.searchReferences.xrepo",
}
return l.codeIntelligenceWeeklyUsersCount(ctx, eventNames, time.Now().UTC())
}
func (l *eventLogStore) CodeIntelligencePreciseCrossRepositoryWAUs(ctx context.Context) (int, error) {
eventNames := []string{
"codeintel.lsifDefinitions.xrepo",
"codeintel.lsifReferences.xrepo",
}
return l.codeIntelligenceWeeklyUsersCount(ctx, eventNames, time.Now().UTC())
}
func (l *eventLogStore) CodeIntelligenceSearchBasedCrossRepositoryWAUs(ctx context.Context) (int, error) {
eventNames := []string{
"codeintel.searchDefinitions.xrepo",
"codeintel.searchReferences.xrepo",
}
return l.codeIntelligenceWeeklyUsersCount(ctx, eventNames, time.Now().UTC())
}
func (l *eventLogStore) codeIntelligenceWeeklyUsersCount(ctx context.Context, eventNames []string, now time.Time) (wau int, _ error) {
var names []*sqlf.Query
for _, name := range eventNames {
names = append(names, sqlf.Sprintf("%s", name))
}
if err := l.QueryRow(ctx, sqlf.Sprintf(codeIntelWeeklyUsersQuery, now, sqlf.Join(names, ", "))).Scan(&wau); err != nil {
return 0, err
}
return wau, nil
}
var codeIntelWeeklyUsersQuery = `
-- source: internal/database/event_logs.go:codeIntelligenceWeeklyUsersCount
SELECT COUNT(DISTINCT ` + userIDQueryFragment + `)
FROM event_logs
WHERE
timestamp >= ` + makeDateTruncExpression("week", "%s::timestamp") + `
AND name IN (%s);
`
type CodeIntelligenceRepositoryCounts struct {
NumRepositories int
NumRepositoriesWithUploadRecords int
NumRepositoriesWithFreshUploadRecords int
NumRepositoriesWithIndexRecords int
NumRepositoriesWithFreshIndexRecords int
NumRepositoriesWithAutoIndexConfigurationRecords int
}
func (l *eventLogStore) CodeIntelligenceRepositoryCounts(ctx context.Context) (counts CodeIntelligenceRepositoryCounts, err error) {
rows, err := l.Query(ctx, sqlf.Sprintf(codeIntelligenceRepositoryCountsQuery))
if err != nil {
return CodeIntelligenceRepositoryCounts{}, err
}
defer rows.Close()
for rows.Next() {
if err := rows.Scan(
&counts.NumRepositories,
&counts.NumRepositoriesWithUploadRecords,
&counts.NumRepositoriesWithFreshUploadRecords,
&counts.NumRepositoriesWithIndexRecords,
&counts.NumRepositoriesWithFreshIndexRecords,
&counts.NumRepositoriesWithAutoIndexConfigurationRecords,
); err != nil {
return CodeIntelligenceRepositoryCounts{}, err
}
}
if err := rows.Err(); err != nil {
return CodeIntelligenceRepositoryCounts{}, err
}
return counts, nil
}
var codeIntelligenceRepositoryCountsQuery = `
-- source: internal/database/event_logs.go:CodeIntelligenceRepositoryCounts
SELECT
(SELECT COUNT(*) FROM repo r WHERE r.deleted_at IS NULL)
AS num_repositories,
(SELECT COUNT(DISTINCT u.repository_id) FROM lsif_dumps_with_repository_name u)
AS num_repositories_with_upload_records,
(SELECT COUNT(DISTINCT u.repository_id) FROM lsif_dumps_with_repository_name u WHERE u.uploaded_at >= NOW() - '168 hours'::interval)
AS num_repositories_with_fresh_upload_records,
(SELECT COUNT(DISTINCT u.repository_id) FROM lsif_indexes_with_repository_name u WHERE u.state = 'completed')
AS num_repositories_with_index_records,
(SELECT COUNT(DISTINCT u.repository_id) FROM lsif_indexes_with_repository_name u WHERE u.state = 'completed' AND u.queued_at >= NOW() - '168 hours'::interval)
AS num_repositories_with_fresh_index_records,
(SELECT COUNT(DISTINCT uc.repository_id) FROM lsif_index_configuration uc WHERE uc.autoindex_enabled IS TRUE AND data IS NOT NULL)
AS num_repositories_with_index_configuration_records
`
type CodeIntelligenceRepositoryCountsForLanguage struct {
NumRepositoriesWithUploadRecords int
NumRepositoriesWithFreshUploadRecords int
NumRepositoriesWithIndexRecords int
NumRepositoriesWithFreshIndexRecords int
}
func (l *eventLogStore) CodeIntelligenceRepositoryCountsByLanguage(ctx context.Context) (_ map[string]CodeIntelligenceRepositoryCountsForLanguage, err error) {
rows, err := l.Query(ctx, sqlf.Sprintf(codeIntelligenceRepositoryCountsByLanguageQuery))
if err != nil {
return nil, err
}
defer rows.Close()
var (
language string
numRepositoriesWithUploadRecords,
numRepositoriesWithFreshUploadRecords,
numRepositoriesWithIndexRecords,
numRepositoriesWithFreshIndexRecords *int
)
byLanguage := map[string]CodeIntelligenceRepositoryCountsForLanguage{}
for rows.Next() {
if err := rows.Scan(
&language,
&numRepositoriesWithUploadRecords,
&numRepositoriesWithFreshUploadRecords,
&numRepositoriesWithIndexRecords,
&numRepositoriesWithFreshIndexRecords,
); err != nil {
return nil, err
}
byLanguage[language] = CodeIntelligenceRepositoryCountsForLanguage{
NumRepositoriesWithUploadRecords: safeDerefIntPtr(numRepositoriesWithUploadRecords),
NumRepositoriesWithFreshUploadRecords: safeDerefIntPtr(numRepositoriesWithFreshUploadRecords),
NumRepositoriesWithIndexRecords: safeDerefIntPtr(numRepositoriesWithIndexRecords),
NumRepositoriesWithFreshIndexRecords: safeDerefIntPtr(numRepositoriesWithFreshIndexRecords),
}
}
if err := rows.Err(); err != nil {
return nil, err
}
return byLanguage, nil
}
func safeDerefIntPtr(v *int) int {
if v != nil {
return *v
}
return 0
}
var codeIntelligenceRepositoryCountsByLanguageQuery = `
-- source: internal/database/event_logs.go:CodeIntelligenceRepositoryCountsByLanguage
SELECT
-- Clean up indexer by removing sourcegraph/ docker image prefix for auto-index
-- records, as well as any trailing git tag. This should make all of the in-house
-- indexer names the same on both lsif_uploads and lsif_indexes records.
REGEXP_REPLACE(REGEXP_REPLACE(indexer, '^sourcegraph/', ''), ':\w+$', '') AS indexer,
max(num_repositories_with_upload_records) AS num_repositories_with_upload_records,
max(num_repositories_with_fresh_upload_records) AS num_repositories_with_fresh_upload_records,
max(num_repositories_with_index_records) AS num_repositories_with_index_records,
max(num_repositories_with_fresh_index_records) AS num_repositories_with_fresh_index_records
FROM (
(SELECT u.indexer, COUNT(DISTINCT u.repository_id), NULL::integer, NULL::integer, NULL::integer
FROM lsif_dumps_with_repository_name u GROUP BY u.indexer)
UNION
(SELECT u.indexer, NULL::integer, COUNT(DISTINCT u.repository_id), NULL::integer, NULL::integer
FROM lsif_dumps_with_repository_name u WHERE u.uploaded_at >= NOW() - '168 hours'::interval GROUP BY u.indexer)
UNION
(SELECT u.indexer, NULL::integer, NULL::integer, COUNT(DISTINCT u.repository_id), NULL::integer
FROM lsif_indexes_with_repository_name u WHERE state = 'completed' GROUP BY u.indexer)
UNION
(SELECT u.indexer, NULL::integer, NULL::integer, NULL::integer, COUNT(DISTINCT u.repository_id)
FROM lsif_indexes_with_repository_name u WHERE state = 'completed' AND u.queued_at >= NOW() - '168 hours'::interval GROUP BY u.indexer)
) s(
indexer,
num_repositories_with_upload_records,
num_repositories_with_fresh_upload_records,
num_repositories_with_index_records,
num_repositories_with_fresh_index_records
)
GROUP BY REGEXP_REPLACE(REGEXP_REPLACE(indexer, '^sourcegraph/', ''), ':\w+$', '')
`
func (l *eventLogStore) CodeIntelligenceSettingsPageViewCount(ctx context.Context) (int, error) {
return l.codeIntelligenceSettingsPageViewCount(ctx, time.Now().UTC())
}
func (l *eventLogStore) codeIntelligenceSettingsPageViewCount(ctx context.Context, now time.Time) (int, error) {
pageNames := []string{
"CodeIntelUploadsPage",
"CodeIntelUploadPage",
"CodeIntelIndexesPage",
"CodeIntelIndexPage",
"CodeIntelConfigurationPage",
"CodeIntelConfigurationPolicyPage",
}
names := make([]*sqlf.Query, 0, len(pageNames))
for _, pageName := range pageNames {
names = append(names, sqlf.Sprintf("%s", fmt.Sprintf("View%s", pageName)))
}
count, _, err := basestore.ScanFirstInt(l.Query(ctx, sqlf.Sprintf(codeIntelligenceSettingsPageViewCountQuery, sqlf.Join(names, ","), now)))
return count, err
}
var codeIntelligenceSettingsPageViewCountQuery = `
-- source: internal/database/event_logs.go:CodeIntelligenceSettingsPageViewCount
SELECT COUNT(*) FROM event_logs WHERE name IN (%s) AND timestamp >= ` + makeDateTruncExpression("week", "%s::timestamp")
func (l *eventLogStore) AggregatedCodeIntelEvents(ctx context.Context) ([]types.CodeIntelAggregatedEvent, error) {
return l.aggregatedCodeIntelEvents(ctx, time.Now().UTC())
}
func (l *eventLogStore) aggregatedCodeIntelEvents(ctx context.Context, now time.Time) (events []types.CodeIntelAggregatedEvent, err error) {
var eventNames = []string{
"codeintel.lsifHover",
"codeintel.lsifDefinitions",
"codeintel.lsifDefinitions.xrepo",
"codeintel.lsifReferences",
"codeintel.lsifReferences.xrepo",
"codeintel.searchHover",
"codeintel.searchDefinitions",
"codeintel.searchDefinitions.xrepo",
"codeintel.searchReferences",
"codeintel.searchReferences.xrepo",
}
var eventNameQueries []*sqlf.Query
for _, name := range eventNames {
eventNameQueries = append(eventNameQueries, sqlf.Sprintf("%s", name))
}
query := sqlf.Sprintf(aggregatedCodeIntelEventsQuery, now, now, sqlf.Join(eventNameQueries, ", "))
rows, err := l.Query(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var event types.CodeIntelAggregatedEvent
err := rows.Scan(
&event.Name,
&event.LanguageID,
&event.Week,
&event.TotalWeek,
&event.UniquesWeek,
)
if err != nil {
return nil, err
}
events = append(events, event)
}
if err = rows.Err(); err != nil {
return nil, err
}
return events, nil
}
var aggregatedCodeIntelEventsQuery = `
-- source: internal/database/event_logs.go:aggregatedCodeIntelEvents
WITH events AS (
SELECT
name,
(argument->>'languageId')::text as language_id,
` + aggregatedUserIDQueryFragment + ` AS user_id,
` + makeDateTruncExpression("week", "%s::timestamp") + ` as current_week
FROM event_logs
WHERE
timestamp >= ` + makeDateTruncExpression("week", "%s::timestamp") + `
AND name IN (%s)
)
SELECT
name,
language_id,
current_week,
COUNT(*) AS total_week,
COUNT(DISTINCT user_id) AS uniques_week
FROM events GROUP BY name, current_week, language_id;
`
func (l *eventLogStore) AggregatedSearchEvents(ctx context.Context, now time.Time) ([]types.SearchAggregatedEvent, error) {
latencyEvents, err := l.aggregatedSearchEvents(ctx, aggregatedSearchLatencyEventsQuery, now)
if err != nil {
return nil, err
}
usageEvents, err := l.aggregatedSearchEvents(ctx, aggregatedSearchUsageEventsQuery, now)
if err != nil {
return nil, err
}
return append(latencyEvents, usageEvents...), nil
}
func (l *eventLogStore) aggregatedSearchEvents(ctx context.Context, queryString string, now time.Time) (events []types.SearchAggregatedEvent, err error) {
query := sqlf.Sprintf(queryString, now, now, now, now)
rows, err := l.Query(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var event types.SearchAggregatedEvent
err := rows.Scan(
&event.Name,
&event.Month,
&event.Week,
&event.Day,
&event.TotalMonth,
&event.TotalWeek,
&event.TotalDay,
&event.UniquesMonth,
&event.UniquesWeek,
&event.UniquesDay,
pq.Array(&event.LatenciesMonth),
pq.Array(&event.LatenciesWeek),
pq.Array(&event.LatenciesDay),
)
if err != nil {
return nil, err
}
events = append(events, event)
}
if err = rows.Err(); err != nil {
return nil, err
}
return events, nil
}
var searchLatencyEventNames = []string{
"'search.latencies.literal'",
"'search.latencies.regexp'",
"'search.latencies.structural'",
"'search.latencies.file'",
"'search.latencies.repo'",
"'search.latencies.diff'",
"'search.latencies.commit'",
"'search.latencies.symbol'",
}
var aggregatedSearchLatencyEventsQuery = `
-- source: internal/database/event_logs.go:aggregatedSearchLatencyEvents
WITH events AS (
SELECT
name,
-- Postgres 9.6 needs to go from text to integer (i.e. can't go directly to integer)
(argument->'durationMs')::text::integer as latency,
` + aggregatedUserIDQueryFragment + ` AS user_id,
` + makeDateTruncExpression("month", "timestamp") + ` as month,
` + makeDateTruncExpression("week", "timestamp") + ` as week,
` + makeDateTruncExpression("day", "timestamp") + ` as day,
` + makeDateTruncExpression("month", "%s::timestamp") + ` as current_month,
` + makeDateTruncExpression("week", "%s::timestamp") + ` as current_week,
` + makeDateTruncExpression("day", "%s::timestamp") + ` as current_day
FROM event_logs
WHERE
timestamp >= ` + makeDateTruncExpression("month", "%s::timestamp") + `
AND name IN (` + strings.Join(searchLatencyEventNames, ", ") + `)
)
SELECT
name,
current_month,
current_week,
current_day,
COUNT(*) FILTER (WHERE month = current_month) AS total_month,
COUNT(*) FILTER (WHERE week = current_week) AS total_week,
COUNT(*) FILTER (WHERE day = current_day) AS total_day,
COUNT(DISTINCT user_id) FILTER (WHERE month = current_month) AS uniques_month,
COUNT(DISTINCT user_id) FILTER (WHERE week = current_week) AS uniques_week,
COUNT(DISTINCT user_id) FILTER (WHERE day = current_day) AS uniques_day,
PERCENTILE_CONT(ARRAY[0.50, 0.90, 0.99]) WITHIN GROUP (ORDER BY latency) FILTER (WHERE month = current_month) AS latencies_month,
PERCENTILE_CONT(ARRAY[0.50, 0.90, 0.99]) WITHIN GROUP (ORDER BY latency) FILTER (WHERE week = current_week) AS latencies_week,
PERCENTILE_CONT(ARRAY[0.50, 0.90, 0.99]) WITHIN GROUP (ORDER BY latency) FILTER (WHERE day = current_day) AS latencies_day
FROM events GROUP BY name, current_month, current_week, current_day
`
var aggregatedSearchUsageEventsQuery = `
-- source: internal/database/event_logs.go:aggregatedSearchUsageEvents
WITH events AS (
SELECT
json.key::text,
json.value::text,
` + aggregatedUserIDQueryFragment + ` AS user_id,
` + makeDateTruncExpression("month", "timestamp") + ` as month,
` + makeDateTruncExpression("week", "timestamp") + ` as week,
` + makeDateTruncExpression("day", "timestamp") + ` as day,
` + makeDateTruncExpression("month", "%s::timestamp") + ` as current_month,
` + makeDateTruncExpression("week", "%s::timestamp") + ` as current_week,
` + makeDateTruncExpression("day", "%s::timestamp") + ` as current_day
FROM event_logs
CROSS JOIN LATERAL jsonb_each(argument->'code_search'->'query_data'->'query') json
WHERE
timestamp >= ` + makeDateTruncExpression("month", "%s::timestamp") + `
AND name = 'SearchResultsQueried'
)
SELECT
key,
current_month,
current_week,
current_day,
SUM(case when month = current_month then value::int else 0 end) AS total_month,
SUM(case when week = current_week then value::int else 0 end) AS total_week,
SUM(case when day = current_day then value::int else 0 end) AS total_day,
COUNT(DISTINCT user_id) FILTER (WHERE month = current_month) AS uniques_month,
COUNT(DISTINCT user_id) FILTER (WHERE week = current_week) AS uniques_week,
COUNT(DISTINCT user_id) FILTER (WHERE day = current_day) AS uniques_day,
NULL,
NULL,
NULL
FROM events
WHERE key IN
(
'count_or',
'count_and',
'count_not',
'count_select_repo',
'count_select_file',
'count_select_content',
'count_select_symbol',
'count_select_commit_diff_added',
'count_select_commit_diff_removed',
'count_repo_contains',
'count_repo_contains_file',
'count_repo_contains_content',
'count_repo_contains_commit_after',
'count_count_all',
'count_non_global_context',
'count_only_patterns',
'count_only_patterns_three_or_more'
)
GROUP BY key, current_month, current_week, current_day
`
// userIDQueryFragment is a query fragment that can be used to return the anonymous user ID
// when the user ID is not set (i.e. 0).
const userIDQueryFragment = `
CASE WHEN user_id = 0
THEN anonymous_user_id
ELSE CAST(user_id AS TEXT)
END
`
// aggregatedUserIDQueryFragment is a query fragment that can be used to canonicalize the
// values of the user_id and anonymous_user_id fields (assumed in scope) int a unified value.
const aggregatedUserIDQueryFragment = `
CASE WHEN user_id = 0
-- It's faster to group by an int rather than text, so we convert
-- the anonymous_user_id to an int, rather than the user_id to text.
THEN ('x' || substr(md5(anonymous_user_id), 1, 8))::bit(32)::int
ELSE user_id
END
`
// makeDateTruncExpression returns an expresson that converts the given
// SQL expression into the start of the containing date container specified
// by the unit parameter (e.g. day, week, month, or).
func makeDateTruncExpression(unit, expr string) string {
if unit == "week" {
return fmt.Sprintf(`DATE_TRUNC('%s', TIMEZONE('UTC', %s) + '1 day'::interval) - '1 day'::interval`, unit, expr)
}
return fmt.Sprintf(`DATE_TRUNC('%s', TIMEZONE('UTC', %s))`, unit, expr)
}