Skip to content

Commit a4847a1

Browse files
committed
Improve metrics for builds that fail
Adds a `failure_detail` field in addition to the existing `failure_reason`, which contains additional context relevant to the failure where available. This will make it easier to find trends in the most frequent user-caused failure modes (eg invalid Python version specifier) so I can then adjust error messages/docs/implementation to improve UX. This context sometimes contain user input, so the values saved to the metadata store now also have additional escaping and validation performed before writing the value (in addition to the existing YAML escaping performed in `bin/report`). GUS-W-17800067.
1 parent c097dfe commit a4847a1

File tree

7 files changed

+39
-6
lines changed

7 files changed

+39
-6
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [Unreleased]
44

5+
- Improved buildpack metrics for builds that fail. ([#1746](https://github.com/heroku/heroku-buildpack-python/pull/1746))
56

67
## [v276] - 2025-02-05
78

bin/report

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ kv_pair_string() {
6262
STRING_FIELDS=(
6363
cache_status
6464
django_collectstatic
65+
failure_detail
6566
failure_reason
6667
nltk_downloader
6768
package_manager

bin/steps/python

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ if ! curl --output /dev/null --silent --head --fail --retry 3 --retry-connrefuse
2323
https://devcenter.heroku.com/articles/python-support#supported-python-versions
2424
EOF
2525
meta_set "failure_reason" "python-version-not-found"
26+
meta_set "failure_detail" "${python_full_version}"
2627
exit 1
2728
fi
2829

@@ -42,6 +43,7 @@ else
4243
Please try again and to see if the error resolves itself.
4344
EOF
4445
meta_set "failure_reason" "python-download"
46+
# TODO: Set failure_detail here once refactored.
4547
exit 1
4648
fi
4749

lib/checks.sh

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ function checks::ensure_supported_stack() {
2020
Upgrade to a newer stack to continue using this buildpack.
2121
EOF
2222
meta_set "failure_reason" "stack::eol"
23+
meta_set "failure_detail" "${stack}"
2324
exit 1
2425
;;
2526
*)
@@ -34,6 +35,7 @@ function checks::ensure_supported_stack() {
3435
https://devcenter.heroku.com/articles/managing-buildpacks#classic-buildpacks-references
3536
EOF
3637
meta_set "failure_reason" "stack::unknown"
38+
meta_set "failure_detail" "${stack}"
3739
exit 1
3840
;;
3941
esac

lib/kvstore.sh

+13-1
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,20 @@ kv_set() {
2323
# TODO: Stop ignoring an incorrect number of passed arguments.
2424
if [[ $# -eq 3 ]]; then
2525
local f="${1}"
26+
local key="${2}"
27+
28+
# Truncate the value to an arbitrary 100 characters since it will sometimes contain user-provided
29+
# inputs which may be unbounded in size. Ideally individual call sites will perform more aggressive
30+
# truncation themselves based on the expected value size, however this is here as a fallback.
31+
# (Honeycomb supports string fields up to 64KB in size, however, it's not worth filling up the
32+
# metadata store or bloating the payload passed back to Vacuole/submitted to Honeycomb given the
33+
# extra content in those cases is not normally useful.)
34+
local value="${3:0:100}"
35+
# Replace newlines since the data store file format requires that keys don't span multiple lines.
36+
value="${value//$'\n'/ }"
37+
2638
if [[ -f "${f}" ]]; then
27-
echo "${2}=${3}" >>"${f}"
39+
echo "${key}=${value}" >>"${f}"
2840
fi
2941
fi
3042
}

lib/python_version.sh

+16-3
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ function python_version::parse_runtime_txt() {
104104
in the correct format.
105105
106106
The following file contents were found, which aren't valid:
107-
${contents}
107+
${contents:0:100}
108108
109109
However, the runtime.txt file is deprecated since it has
110110
been replaced by the .python-version file. As such, we
@@ -125,6 +125,7 @@ function python_version::parse_runtime_txt() {
125125
your app to receive Python security updates.
126126
EOF
127127
meta_set "failure_reason" "runtime-txt::invalid-version"
128+
meta_set "failure_detail" "${contents:0:50}"
128129
exit 1
129130
fi
130131
}
@@ -174,6 +175,7 @@ function python_version::parse_python_version_file() {
174175
your app to receive Python security updates.
175176
EOF
176177
meta_set "failure_reason" "python-version-file::invalid-version"
178+
meta_set "failure_detail" "${line:0:50}"
177179
exit 1
178180
fi
179181
;;
@@ -193,17 +195,19 @@ function python_version::parse_python_version_file() {
193195
begin with a '#', otherwise it will be treated as a comment.
194196
EOF
195197
meta_set "failure_reason" "python-version-file::no-version"
198+
meta_set "failure_detail" "${contents:0:50}"
196199
exit 1
197200
;;
198201
*)
202+
local first_five_version_lines=("${version_lines[@]:0:5}")
199203
output::error <<-EOF
200204
Error: Invalid Python version in .python-version.
201205
202206
Multiple versions were found in your .python-version file:
203207
204208
$(
205209
IFS=$'\n'
206-
echo "${version_lines[*]}"
210+
echo "${first_five_version_lines[*]}"
207211
)
208212
209213
Update the file so it contains only one Python version.
@@ -212,6 +216,10 @@ function python_version::parse_python_version_file() {
212216
lines begin with a '#', so that they are ignored.
213217
EOF
214218
meta_set "failure_reason" "python-version-file::multiple-versions"
219+
meta_set "failure_detail" "$(
220+
IFS=,
221+
echo "${first_five_version_lines[*]}"
222+
)"
215223
exit 1
216224
;;
217225
esac
@@ -233,17 +241,19 @@ function python_version::read_pipenv_python_version() {
233241
fi
234242

235243
if ! version=$(jq --raw-output '._meta.requires.python_full_version // ._meta.requires.python_version' "${pipfile_lock_path}" 2>&1); then
244+
local jq_error_message="${version}"
236245
output::error <<-EOF
237246
Error: Can't parse Pipfile.lock.
238247
239248
A Pipfile.lock file was found, however, it couldn't be parsed:
240-
${version}
249+
${jq_error_message}
241250
242251
This is likely due to it not being valid JSON.
243252
244253
Run 'pipenv lock' to regenerate/fix the lockfile.
245254
EOF
246255
meta_set "failure_reason" "pipfile-lock::invalid-json"
256+
meta_set "failure_detail" "${jq_error_message:0:100}"
247257
exit 1
248258
fi
249259

@@ -282,6 +292,7 @@ function python_version::read_pipenv_python_version() {
282292
https://pipenv.pypa.io/en/stable/specifiers.html#specifying-versions-of-python
283293
EOF
284294
meta_set "failure_reason" "pipfile-lock::invalid-version"
295+
meta_set "failure_detail" "${version:0:50}"
285296
exit 1
286297
fi
287298
}
@@ -341,6 +352,7 @@ function python_version::resolve_python_version() {
341352
EOF
342353
fi
343354
meta_set "failure_reason" "python-version::eol"
355+
meta_set "failure_detail" "${major}.${minor}"
344356
exit 1
345357
fi
346358

@@ -388,6 +400,7 @@ function python_version::resolve_python_version() {
388400
EOF
389401
fi
390402
meta_set "failure_reason" "python-version::unknown-major"
403+
meta_set "failure_detail" "${major}.${minor}"
391404
exit 1
392405
fi
393406

lib/utils.sh

+4-2
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ function utils::bundled_pip_module_path() {
4848
}
4949

5050
function utils::abort_internal_error() {
51-
local message="${1}"
51+
local message
52+
message="${1} (line $(caller || true))"
5253
output::error <<-EOF
53-
Internal error: ${message} (line $(caller || true)).
54+
Internal error: ${message}.
5455
EOF
5556
meta_set "failure_reason" "internal-error"
57+
meta_set "failure_detail" "${message}"
5658
exit 1
5759
}

0 commit comments

Comments
 (0)